Run secrets resolve
POST …/state/runs/{runId}/secrets/resolve is the only route on the platform that returns secret values to a machine. A running job hands it the scope references its plan declared and receives the decrypted values — if, and only if, two independent gates both pass.
POST /v1/organizations/{orgId}/projects/{projectId}/state/runs/{runId}/secrets/resolve
Two gates
- Bearer authorization — the caller's token must hold
secret.value.usefor this workspace + project (workflow actors by token-bound scope; users and service principals via role policy). Denied as a resource-hiding404. - A live job lease — the body's
(runId, jobId, runnerId, leaseEpoch)must match a lease that is live right now. A lapsed or reassigned lease answers409 lease_lost, the same envelope as the:heartbeat/:completeverbs.
The gates are independent by design: a stolen bearer token alone can never pull values, because it does not hold a live lease on a claimed job.
Request
{
"runnerId": "runner-8f2c",
"jobId": "api.prod.deploy",
"leaseEpoch": 1,
"refs": [
"secret://acme/project:storefront/env:prod/STRIPE_API_KEY",
"secret://acme/project:storefront/env:prod/DATABASE_URL@3",
"secret://acme/project:storefront/env:stage/SMOKE_TEST_TOKEN"
],
"optionalRefs": [
"secret://acme/project:storefront/env:prod/SENTRY_DSN"
]
}
Every reference must arrive fully pinned — contextual omission is bound by the CLI at expand time, before the wire. The workspace and project segments must name this route's own scope (verified against membership and projects, never trusted textually); a component: segment is an assertion cross-checked against the lease-derived component from the leased job row — a mismatch is 422, and the segment is never a grant.
Response
{
"data": {
"secrets": {
"STRIPE_API_KEY": "sk_live_…",
"DATABASE_URL": "postgres://…",
"SMOKE_TEST_TOKEN": "tok_…",
"SENTRY_DSN": "https://…"
},
"secretsByEnv": {
"prod": {
"STRIPE_API_KEY": "sk_live_…",
"DATABASE_URL": "postgres://…",
"SENTRY_DSN": "https://…"
},
"stage": {
"SMOKE_TEST_TOKEN": "tok_…"
}
},
"ttlSeconds": 300
},
"meta": { "requestId": "req_5f2d1c0b9a8e7f6d5c4b3a35", "cursor": null }
}
ttlSeconds (300) is how long the runner may cache served values.
Multi-environment grouping
One resolve call may carry references for several environments — the deploy shape where a stage deploy renders a template whose prod section needs prod's wiring too. References group by their env: dimension, and each group resolves with its own environment id: policy is evaluated once per environment, so grouping changes fan-out, never evaluation scope.
secretsByEnvmaps environment slug → key → value and is collision-free.- The flat
secretsmap is retained for older clients. A key served for two environments keeps the last group's value (declaration order) there — env-aware clients should readsecretsByEnv.
Caps and validation
| Rule | Failure |
|---|---|
At most 50 refs per resolve — refs and optionalRefs share the cap | 422 |
| At most 8 distinct environments per resolve | 422 — "At most 8 distinct environments per resolve" |
| Unknown environment slug | 404 (resource-hiding, like every other miss) |
| Same key pinned at conflicting versions within one environment | 422 |
| Malformed reference | 422 — the error names the parse-failure class but never echoes the input, since a malformed value in a secret-shaped slot may itself be a secret |
Optional references
optionalRefs carry the same grammar, grouping, and environment constraints as refs, with one difference: a key that has no head at its rung is simply skipped — absent from the response — instead of failing the whole resolve with a 404. This is the wire-now-seed-later shape: a plan can reference a secret that a later job (or a human) will publish, without the first run failing on its absence.
Everything else still fails hard:
- A policy-denied key fails the resolve — optional softens absence, never denial.
- An unservable version pin fails the resolve.
- A key requested both hard and optional stays fail-closed — hard wins.
- A resolve of only optional refs is legal, but a malformed value present in
refsis never masked by it.