fix(secrets): project agent API grants under their access alias

listAgentSecretAccess grouped an agent's bindings by the underlying
secretId and always reported company_secrets.key, so an approved
access.<ALIAS> grant was listed under the managed secret's own key and
several aliases over one reused secret collapsed into a single entry.
An API-only alias on a shared managed secret was therefore unreachable
from a governed run.

Project each binding under the key an agent actually addresses it by
instead: env.* keeps projecting the managed secret key, access.<ALIAS>
projects the alias, normalized the same way managed keys are. Grouping
by (secretId, projected key) keeps distinct aliases separate while an
alias equal to its secret's key still merges with the env grant into a
single delivery: "both" entry. Selection within a group prefers the API
binding, so POST /api/agents/me/secrets/:key/value resolves the exact
projected binding; all run-bound authorization, audit, and redaction
checks are unchanged.

Covered by service, route, and end-to-end route tests for a mismatched
managed key vs alias, two aliases over one secret, and the env/API
pair that shares a projected key. Tests use fixtures only.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-16 19:17:15 +00:00
1 parent fd472d02ba
commit bf3c7b08c4
5 files changed
+409 -24

No files matched your search

+10
View File
@@ -38,6 +38,16 @@ GET /api/agents/me/secrets
IDs, binding IDs, or config paths. An `env.*` binding implies read access through
this API; an `access.*` binding grants API access without environment injection.
`key` is the key the grant is *projected* under, which is not always the managed
secret's own key. An `env.*` binding is listed under the managed secret key it is
injected from, while an `access.<ALIAS>` binding is listed under `<ALIAS>`,
normalized the same way managed secret keys are (lower-cased). So one managed
secret can back several grants — for example `access.EVALS_OPENAI_API_KEY` and
`access.RUNNERD_OPENAI_API_KEY` over a single shared key are listed as two
separate `api` entries, each fetched by its own alias. A secret that has both an
`env.*` binding and an `access.*` alias equal to its key is listed once, as
`both`.
Fetch a value only when it is needed. The request has no body and the response
uses `Cache-Control: no-store`: