> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polycore.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Runner configuration

> The committed capability config, runtime bindings, and repository layout for a customer-hosted runner.

The runner separates reviewed structure from deployment-time identity and
secrets.

* `polycore.json` declares the capability surface and project context.
* The `actions/` directory contains customer-authored capabilities.
* Runtime variables bind one deployed process to one project and environment.
* Your deployment resolves credentials through workload identity or secret
  injection.

<Info>
  Polycore prepares this package with you during early access. This reference
  exists so engineering and security teams can review exactly what is committed
  and what remains runtime-only.
</Info>

## Repository layout

<Tree>
  <Tree.Folder name="polycore-runner" defaultOpen>
    <Tree.File name="polycore.json" />

    <Tree.Folder name="actions">
      <Tree.Folder name="billing">
        <Tree.Folder name="set-plan">
          <Tree.File name="action.ts" />
        </Tree.Folder>
      </Tree.Folder>
    </Tree.Folder>

    <Tree.Folder name="context">
      <Tree.File name="product.md" />
    </Tree.Folder>

    <Tree.Folder name="schema">
      <Tree.File name="typesync-definition.json" />
    </Tree.Folder>

    <Tree.File name="Dockerfile" />

    <Tree.File name="package.json" />
  </Tree.Folder>
</Tree>

Only include folders that the integration needs. Actions are discovered by
convention from `actions/` beside `polycore.json`; there is no action-directory
setting.

## Complete example

```json polycore.json theme={"system"}
{
  "integrations": {
    "firebase": {
      "firestore": {
        "typesync": "./schema/typesync-definition.json",
        "write": { "keyEnv": "FIRESTORE_WRITE_KEY" }
      },
      "auth": {},
      "storage": {
        "buckets": ["your-project.firebasestorage.app"],
        "limits": { "maxSignedUrlMinutes": 15 }
      }
    },
    "postgres": {
      "limits": { "maxRows": 500 }
    },
    "posthog": {
      "host": "https://us.posthog.com"
    }
  },
  "context": { "file": "./context/product.md" }
}
```

Relative file paths resolve from the directory containing `polycore.json`, not
from the process's current working directory.

## Four conventions

The file follows the same four rules throughout, so a family you have not used
before behaves the way you would guess.

<Steps>
  <Step title="Presence enables reads">
    `"auth": {}` exposes the Auth read family. There is no `true` shorthand:
    one shape per family means nothing has to ask whether a config is a boolean
    or an object.
  </Step>

  <Step title="`write` is the write grant, and it names the identity">
    Absent means read-only. `"ambient"` uses Application Default Credentials.
    `{ "keyEnv": "…" }` uses a separate service-account key, so the runtime
    identity can stay read-only at IAM. Everything a grant unlocks is
    `write`-hazard and still needs a human approval per call.
  </Step>

  <Step title="Every environment binding is a `*Env` field">
    No variable name is hardcoded in the runner, so you can rename any of them
    without a code change. Most default to one conventional name; the Firebase
    project id instead falls back to Google's own resolution order, because a
    GCP runtime already populates it under more than one name.
  </Step>

  <Step title="Every ceiling lives under `limits`">
    If a value bounds how much work one call may do, it is in that object and
    nowhere else.
  </Step>
</Steps>

<Note>
  `integrations` here means **your** systems, reached only from inside your
  environment. It is unrelated to Polycore's hosted integrations (Notion and
  friends), which are provider grants the control plane holds.
</Note>

## Configuration fields

<ParamField path="integrations" type="object" default="{}">
  The customer systems this runner exposes. Every family is read-only unless it
  carries a `write` grant. Omit for a runner that serves only customer-authored
  actions.
</ParamField>

### Firebase

<ParamField path="integrations.firebase" type="object">
  The Firebase project this runner serves, and which of its services are
  exposed. The three services are independent grants against one project: a
  runner may serve document reads without ever exposing the auth directory.
</ParamField>

<ParamField path="integrations.firebase.projectIdEnv" type="string">
  The variable holding the GCP project id, when you want to name one. Omit it
  and the runner uses Google's own resolution order — `FIRESTORE_PROJECT_ID`,
  then `GOOGLE_CLOUD_PROJECT`, then `GCLOUD_PROJECT` — which is what GCP
  runtimes already populate. The explicit variable comes first on purpose: an
  operator who sets one means it, and it should not lose to whichever project
  the VM happens to sit in.

  Name a variable when the runner reads a *different* Firebase project than the
  one it runs in, or when your deployment uses its own spelling. Naming one
  means *only* that one: there is no quiet fallback behind it.
</ParamField>

<ParamField path="integrations.firebase.firestore" type="object">
  Exposes `firestore.*` document reads. See
  [Firestore](/integrations/firestore).
</ParamField>

<ParamField path="integrations.firebase.firestore.typesync" type="string">
  Path to a build-generated Typesync definition JSON, relative to
  `polycore.json`. The runner validates and reads this file locally, and it
  grounds the agent in your declared schema instead of one inferred from sampled
  documents.
</ParamField>

<ParamField path="integrations.firebase.firestore.write" type="&#x22;ambient&#x22; | { keyEnv }">
  Grants the approval-gated `firestore.create` / `set` / `update` / `delete` /
  `batchWrite` capabilities.
</ParamField>

<ParamField path="integrations.firebase.auth" type="object">
  Exposes `auth.*` directory reads. See [Firebase
  Auth](/integrations/firebase-auth).
</ParamField>

<ParamField path="integrations.firebase.auth.write" type="&#x22;ambient&#x22; | { keyEnv }">
  Grants the approval-gated account-administration and action-link capabilities.
  `{keyEnv}` lets the runtime service account stay `roles/firebaseauth.viewer`.
</ParamField>

<ParamField path="integrations.firebase.storage" type="object">
  Exposes `storage.*` object reads. See [Firebase
  Storage](/integrations/firebase-storage).
</ParamField>

<ParamField path="integrations.firebase.storage.defaultBucketEnv" type="string" default="FIREBASE_STORAGE_BUCKET">
  Where the project's default bucket name comes from, used whenever a Storage
  capability names no bucket.
</ParamField>

<ParamField path="integrations.firebase.storage.buckets" type="string[]">
  The buckets in scope. A runtime service account often has object access to
  buckets unrelated to the product (build artifacts, Terraform state, backups);
  naming the ones in scope keeps the capability surface narrower than the IAM
  grant behind it. Absent means every reachable bucket. The default bucket must
  appear in this list when it is set.
</ParamField>

<ParamField path="integrations.firebase.storage.limits.maxSignedUrlMinutes" type="integer" default="60">
  Longest life of a signed read URL. **`0` withdraws `storage.getSignedUrl`
  entirely**, so it is never advertised.
</ParamField>

<ParamField path="integrations.firebase.storage.limits.maxDownloadBytes" type="integer" default="1048576">
  Largest object `storage.download` returns inline. Larger objects need a signed
  URL.
</ParamField>

<ParamField path="integrations.firebase.storage.limits.maxUploadBytes" type="integer" default="1048576">
  Largest inline object `storage.upload` accepts, decoded.
</ParamField>

<ParamField path="integrations.firebase.storage.limits.maxObjectsPerCall" type="integer" default="1000">
  Objects one `storage.summarize` walk covers before it stops and reports
  `complete: false`.
</ParamField>

<ParamField path="integrations.firebase.storage.write" type="&#x22;ambient&#x22; | { keyEnv }">
  Grants the approval-gated `storage.delete` / `copy` / `move` / `setMetadata` /
  `makePublic` / `upload` capabilities. Writes resolve their bucket through the
  same allowlist as reads, so a write can never reach a bucket a read could not.
</ParamField>

### Postgres

<ParamField path="integrations.postgres" type="object">
  Exposes `postgres.*` bounded schema discovery, explain, and parameterized
  read-only queries. See [Postgres](/integrations/postgres).
</ParamField>

<ParamField path="integrations.postgres.urlEnv" type="string" default="POLYCORE_POSTGRES_URL">
  Where the connection string comes from. Point it at a read-only role or
  replica.
</ParamField>

<ParamField path="integrations.postgres.write" type="object">
  Grants `postgres.update` and `postgres.delete`. Absent means read-only, as
  with every other family. Present with no `urlEnv` means writes go through the
  read connection; give `urlEnv` to use a separate write role so the read path
  stays mutation-incapable at the database. See [Postgres
  writes](/integrations/postgres#writes).
</ParamField>

<ParamField path="integrations.postgres.write.urlEnv" type="string">
  Where the write connection string comes from. Omit to reuse
  `integrations.postgres.urlEnv`, which is only appropriate when that role
  already has write access.
</ParamField>

<ParamField path="integrations.postgres.write.limits" type="object">
  Ceilings on write review cost, separate from the read limits because they
  bound what a human can reasonably approve rather than what the database can
  reasonably serve.

  | Limit                | Default | Range    | Meaning                                                                    |
  | -------------------- | ------- | -------- | -------------------------------------------------------------------------- |
  | `maxRows`            | 1,000   | 1–10,000 | Rows one write may name before it is refused rather than previewed.        |
  | `proposalTtlMinutes` | 30      | 1–240    | How long a previewed write stays applicable.                               |
  | `maxDetailRows`      | 50      | 0–1,000  | Row diffs sent for review. The count is always exact; 0 sends counts only. |

  `proposalTtlMinutes` bounds how long a decision stays open, not correctness:
  every previewed row is pinned to the version it had at preview time, so a row
  written in the meantime aborts the apply whatever the clock says.
</ParamField>

<ParamField path="integrations.postgres.limits" type="object">
  Customer-reviewed upper bounds. Every value is a ceiling: a call may request
  less, never more. Unset ceilings take their defaults.

  | Limit                | Default | Range         |
  | -------------------- | ------- | ------------- |
  | `requestTimeoutMs`   | 25,000  | 100–25,000    |
  | `statementTimeoutMs` | 10,000  | 100–25,000    |
  | `acquireTimeoutMs`   | 5,000   | 100–10,000    |
  | `maxRows`            | 1,000   | 1–10,000      |
  | `maxResultBytes`     | 262,144 | 1 KiB–4 MiB   |
  | `poolSize`           | 4       | 1–16          |
  | `maxQueue`           | 16      | 0–100         |
  | `cursorBatchRows`    | 100     | 1–500         |
  | `discoveryLimit`     | 200     | 1–1,000       |
  | `maxSqlBytes`        | 65,536  | 1 KiB–256 KiB |
  | `maxParams`          | 100     | 0–1,000       |
  | `maxParamBytes`      | 131,072 | 1 KiB–1 MiB   |
</ParamField>

### PostHog and Prometheus

<ParamField path="integrations.posthog" type="object">
  Exposes `posthog.*` read capabilities. `host` is committed; the project id and
  API key bind from `projectIdEnv` (default `POSTHOG_PROJECT_ID`) and
  `apiKeyEnv` (default `POSTHOG_PERSONAL_API_KEY`).
</ParamField>

<ParamField path="integrations.posthog.limits.maxLookbackDays" type="integer" default="365">
  Longest window the actor-oriented helpers accept. Raw HogQL stays explicit.
</ParamField>

<ParamField path="integrations.prometheus" type="object">
  Exposes `prometheus.*` read capabilities against `baseUrl`. `auth` is
  `{ "type": "none" }`, `{ "type": "bearer", "tokenEnv": "…" }`, or
  `{ "type": "basic", "usernameEnv": "…", "passwordEnv": "…" }`. An optional
  `tenantHeader` sets a safe header such as `X-Scope-OrgID` from `valueEnv`.
</ParamField>

### Everything else

<ParamField path="context" type="{ file } | { text }">
  Agent grounding for this project. `{ "file": "./context/product.md" }` is
  preferred for anything longer than a line; `{ "text": "…" }` inlines it. Two
  shapes rather than two sibling keys, so "both were set" is not a state the
  parser has to reject.
</ParamField>

<ParamField path="actions.secrets" type="record<string, string>">
  Optional secret values for customer-authored actions, keyed by the names the
  actions declare. Missing keys fall back to `process.env`. Values in the file
  take precedence over the environment.
</ParamField>

<ParamField path="controlPlane" type="object">
  Optional control-plane URL and enrollment identity. Production deployments
  normally supply these fields through environment variables instead.
</ParamField>

<ParamField path="sandbox" type="object">
  Opts the runner into `code.execute` delegated workloads. It sets the workload
  image, OCI runtime (`runsc` by default), container CLI, and maximum CPU,
  memory, process, and timeout limits.
</ParamField>

<Warning>
  Do not commit production values under `actions.secrets` or `controlPlane`.
  Their file forms support local evaluation and explicit configuration, but
  production runner images should receive them from the deployment environment.
</Warning>

<Warning>
  `sandbox` is a preview surface and is absent from standard integrations.
  Enabling it requires explicit host preparation and security review; it adds an
  `unknown`-hazard capability that your policy always holds for human review.
</Warning>

<Warning>
  Do not commit production values under `secrets` or `controlPlane`. Their file
  forms support local evaluation and explicit configuration, but production
  runner images should receive them from the deployment environment.
</Warning>

## Runtime identity

These bindings identify the deployed process and its control-plane enrollment:

<ParamField path="POLYCORE_RUNNER_CONFIG" type="string">
  Path to the committed `polycore.json`. Set this when the runner command does
  not receive `--config <path>`.
</ParamField>

<ParamField path="POLYCORE_PROJECT_SLUG" type="string" required>
  The control-plane project namespace this runner serves.
</ParamField>

<ParamField path="POLYCORE_CONTROL_PLANE_URL" type="string" required>
  The runner WebSocket endpoint, using `wss://` in production.
</ParamField>

<ParamField path="POLYCORE_RUNNER_ID" type="string" required>
  The runner registration provisioned for this project and environment.
</ParamField>

<ParamField path="POLYCORE_JOIN_TOKEN" type="secret" required>
  Authenticates the runner's join handshake.
</ParamField>

<ParamField path="POLYCORE_SIGNING_SECRET" type="secret" required>
  Lets the runner verify dispatch envelopes signed for its registration.
</ParamField>

The environment itself comes from control-plane enrollment. It is not declared
in `polycore.json` and is not selected by capability arguments.

## Datastore bindings

Every one of these is the *default* variable name for a `*Env` field. Rename
any of them in `polycore.json` rather than working around it.

<Tabs>
  <Tab title="Postgres">
    <ParamField path="POLYCORE_POSTGRES_URL" type="secret">
      Required when `integrations.postgres` is present. Point it at a read-only
      role or replica and inject it through your secret manager. Renamed with
      `integrations.postgres.urlEnv`.
    </ParamField>
  </Tab>

  <Tab title="Firebase">
    <ParamField path="FIRESTORE_PROJECT_ID" type="string">
      Explicit GCP project id for every enabled Firebase service, and the first
      one consulted. Set it when the runner reads a different project than the
      one it runs in.
    </ParamField>

    <ParamField path="GOOGLE_CLOUD_PROJECT" type="string">
      Standard GCP runtime variable, already set to the project the runner is
      deployed in. Used when `FIRESTORE_PROJECT_ID` is unset. Application
      Default Credentials supply the identity separately.
    </ParamField>

    <ParamField path="GCLOUD_PROJECT" type="string">
      A further fallback recognized by Google Cloud runtimes.
    </ParamField>

    <ParamField path="(your own)" type="string">
      Name any variable with `integrations.firebase.projectIdEnv` and the three
      above are not consulted at all.
    </ParamField>

    <ParamField path="FIREBASE_STORAGE_BUCKET" type="string">
      Required when `integrations.firebase.storage` is present: the project's
      default bucket, used whenever a Storage capability names none. The runner
      never guesses it — newer projects use
      `<project-id>.firebasestorage.app` and older ones
      `<project-id>.appspot.com`, and a wrong guess reads as an empty bucket
      rather than an error. Renamed with
      `integrations.firebase.storage.defaultBucketEnv`.
    </ParamField>
  </Tab>

  <Tab title="PostHog">
    <ParamField path="POSTHOG_PROJECT_ID" type="string">
      Numeric PostHog project id. Renamed with
      `integrations.posthog.projectIdEnv`.
    </ParamField>

    <ParamField path="POSTHOG_PERSONAL_API_KEY" type="secret">
      Dedicated PostHog read key. Renamed with
      `integrations.posthog.apiKeyEnv`; grant only the query and taxonomy read
      scopes the runner uses.
    </ParamField>
  </Tab>

  <Tab title="Action secrets">
    <ParamField path="POLYCORE_SECRETS" type="secret JSON object">
      Optional runner-level map used to resolve secret keys declared by actions.
      Deployment tooling may provide equivalent local secret bindings.
    </ParamField>

    Each declared key can also be supplied as its own environment variable, such
    as `BILLING_API_TOKEN`. The runner checks the configured secret map first,
    then the matching per-key environment variable.
  </Tab>
</Tabs>

## Project context

Project context explains customer-specific facts the fixed Polycore agent prompt
should not contain. Keep it concise and operational:

```markdown context/product.md theme={"system"}
# Product context

- `accountId` is the stable account identifier.
- Production data uses UTC timestamps.
- "Active" means `status = "active"` and `archivedAt` is absent.
- Never infer an environment from a customer record.
```

Good context covers:

* Domain terms and identifiers.
* Important data-model conventions.
* Safe interpretations of ambiguous language.
* Known limits or preconditions that apply across requests.

Do not place secrets, broad copies of database schemas, or one-off query
instructions in this file. Query tool descriptions and on-demand schema
capabilities should carry platform-specific guidance.

## Configuration lifecycle

<Steps>
  <Step title="Review">
    Query families, context changes, and actions are reviewed in your runner
    repository.
  </Step>

  <Step title="Build">
    The image includes `polycore.json`, actions, context, and any generated
    schema artifact.
  </Step>

  <Step title="Inject">
    CI or infrastructure tooling supplies enrollment and execution secrets
    without rewriting structural config.
  </Step>

  <Step title="Deploy">
    The environment's long-running runner process starts or restarts.
  </Step>

  <Step title="Advertise">
    On connection, the runner sends the current catalog and project context to
    the control plane.
  </Step>
</Steps>

There is deliberately no dashboard write path into `polycore.json`, actions, or
project context. A standing authority change remains a code and deployment
change.

## Environment isolation example

The same runner source can be deployed more than once:

```mermaid theme={"system"}
flowchart TD
  Repo["One reviewed runner repository"]
  Repo --> Dev["Dev deployment<br/>dev runner id<br/>dev identity"]
  Repo --> Prod["Production deployment<br/>prod runner id<br/>prod identity"]
```

Each deployment uses its own runner registration and downstream credentials.
The shared source does not collapse those runtime boundaries.
