Skip to main content
The Firebase Storage integration gives Polycore a native object surface: browse buckets and prefixes, read object metadata, total up usage, pull small files inline, and mint short-lived links to the ones a human needs to open. Behind an explicit write grant and a human approval, it can also delete, copy, move, retag, and re-permission objects. The runner authenticates with Application Default Credentials inside your GCP environment. Polycore cloud does not receive a service-account key.

Why this exists

Uploaded photos, exports, attachments, and crash artifacts are usually the part of a product that ops questions reach for last, because the answer normally requires bucket access nobody wants to hand out for a one-off look.

Two governance controls

Storage differs from the other families in having a blast radius worth tuning, so it carries two controls of its own.

Bucket allowlist

A runtime service account routinely has object access to buckets that have nothing to do with the product: build artifacts, Terraform state, backups. buckets names the ones in scope, so the capability surface is narrower than the IAM grant behind it.

Signed-URL ceiling

storage.getSignedUrl mints a bearer credential that outlives the audited call that created it. limits.maxSignedUrlMinutes caps its life, and setting it to 0 withdraws the capability entirely.

1. Bind the runtime identity

Grant the runner’s service account roles/storage.objectViewer on the buckets in scope. Add roles/storage.objectAdmin only if you intend to enable mutations with ambient credentials.

2. Name the default bucket

This is required, and the runner never guesses it. Newer Firebase projects default to <project-id>.firebasestorage.app and older ones to <project-id>.appspot.com; a wrong guess does not fail loudly, it reads to a caller as an empty bucket.

3. Enable Storage queries

polycore.json
"storage": {} enables the read surface across every bucket the service account can reach, at default ceilings. buckets and limits narrow it.

Available capabilities

read
Lists the buckets this runner can reach, marking the project default. Under an allowlist, the list is the complete scope.
read
Reads a bucket’s location, storage class, versioning, lifecycle rules, and public-access prevention.
read
Lists objects under a prefix. With delimiter: "/" it browses one level at a time, returning folder-like prefixes alongside objects.
read
Reads one object’s size, content type, timestamps, md5, and custom metadata. Returns null when nothing is at that path, which doubles as the existence check.
read
Totals objects under a prefix without listing them: count, bytes, per content-type breakdown, oldest and newest timestamps.
read
Returns a small object’s content inline, as text or base64.
read
Mints a temporary read-only link to one object. Present only when maxSignedUrlMinutes is above zero.
Cloud Storage has no server-side aggregation, so storage.summarize walks objects and stops at maxObjectsPerCall, reporting complete: false when it did. It never presents a partial total as a complete one.
Object paths are flat strings — “folders” are just a shared prefix. A trailing slash is what makes a prefix behave like one.

Optional: generic mutations

Mutations are off until you grant them explicitly.
polycore.json
Every mutation is write-hazard, resolves its bucket through the same allowlist as reads — a write can never reach a bucket a read could not — and names its objects by exact path.
write
Permanently deletes one object by exact path.
write
Deletes up to 100 objects by explicit path. Every path appears in the approval.
write
Copies one object to another path, optionally into another in-scope bucket.
write
Renames or relocates one object within a bucket.
write
Changes content type, cache control, disposition, or custom metadata. The bytes are untouched.
write
Grants allUsers read access to one object.
write
Removes public read access from one object.
write
Writes a small file from inline content. Overwrites an existing object at that path.
storage.makePublic is the only capability in the runner that can expose customer data to the open internet, with no sign-in and no expiry. It is described that way in the approval so a reviewer reads it as the data-disclosure decision it is. To let one person see one file, use storage.getSignedUrl instead — it expires.

No prefix deletes

There is deliberately no “delete everything under this prefix”. That set would resolve after approval, so the approver could not have reviewed it — and a prefix matching three objects when the agent looked can match three thousand by the time a human clicks approve. Bulk work lists the paths first, with no approval needed, and proposes them explicitly.