From 82274c767ca400a876e844df7cb70d0dbc51e329 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 11:09:07 +0000 Subject: [PATCH 1/3] docs(adr): narrow ADR-0090 D10 rule 4 to agent principals; an API key is a credential MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An API key resolves to its owner and to nothing else, so the assembled principal is `human` and carries no `performed_by`. Rule 4's 「every write」 therefore described a reach no door has ever had. Narrow the declared rule to agent principals (the OAuth/MCP client door, the one seam that produces `principalKind: 'agent'`) and add a dated note recording why an API-key caller sits outside it: a credential is how a principal acted, never a second who. Documentary only; no runtime behaviour changes. Claude-Session: https://claude.ai/code/session_01HnRAeVTLJevtQ5iCPX6JSm Co-authored-by: Claude --- ...permission-model-v2-concept-convergence.md | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/adr/0090-permission-model-v2-concept-convergence.md b/docs/adr/0090-permission-model-v2-concept-convergence.md index 8cc5785cab5..51630d5e41e 100644 --- a/docs/adr/0090-permission-model-v2-concept-convergence.md +++ b/docs/adr/0090-permission-model-v2-concept-convergence.md @@ -388,8 +388,12 @@ depends on: permissions, or superuser wildcards; an agent principal never runs `isSystem`. 3. **Human co-sign for `DESTRUCTIVE_OPERATIONS`** regardless of grants — grants decide what an agent may *initiate*, not what it may *complete alone*. - 4. **Dual attribution**: every write records `performed_by` (agent) + `on_behalf_of` (user) + - run id; explain (D6) reports both sides of the intersection. + 4. **Dual attribution**: every write **by an agent principal** records `performed_by` (agent) + + `on_behalf_of` (user) + run id; explain (D6) reports both sides of the intersection. An + agent principal is a caller a door resolves to `principalKind: 'agent'` — today the OAuth / + MCP client door alone. A caller of any other kind records the principal it authenticates as, + and the missing `performed_by` is itself the record that the principal acted for itself; for + the API-key case see the 2026-09-16 note at the end of this decision. Task-scoped, time-boxed agent grants build on the grant-lifecycle follow-up ADR (see *Named follow-ups*). - **`guest`** — resolves to the `guest` position (D9) and nothing else. @@ -398,6 +402,38 @@ The ctx **shape** (kind / audience / onBehalfOf) is a P1 deliverable: it must ex even where evaluation semantics phase in later — retrofitting a principal model post-launch is an alias tax on every API. +> **Note (2026-09-16, #18335) — an API key is its owner's CREDENTIAL, not an agent principal, so +> rule 4 does not reach it.** [ruled] +> +> Rule 4 as first written said 「every write」, which left one caller unclassified: an **API key**. +> Is it an agent acting for its owner, or a tool the owner acted through? It is the tool — decided +> here, not merely observed. +> +> **Why.** A credential is *how* a principal acted, never a second *who*. That is the mainstream +> audit reading, and it is already what the doors do. An agent principal is produced at exactly one +> seam — the `/mcp` OAuth door, where an access token naming an authorized client (`azp`) becomes +> `principalKind: 'agent'` + `onBehalfOf` + `performedBy` +> (`packages/core/src/security/assemble-execution-context.ts#entryFields`). An API key takes a +> different path — `packages/core/src/security/api-key.ts#resolveApiKeyAdmission` into +> `packages/core/src/security/resolve-authz-context.ts#resolveAuthzContext` — which resolves the +> key's OWNER and nothing else. The assembled principal is therefore `human`, carries neither +> `onBehalfOf` nor `performedBy`, and its `sys_audit_log` row is the owner's own. The REST door, +> which is where keys are honoured, hands the assembler no OAuth provenance at all, so an agent +> principal is not representable there by construction. +> +> **What this changes.** Nothing in the runtime — the behaviour above IS the decided behaviour. +> What changes is the DECLARATION: rule 4's 「every write」 is narrowed to agent principals in the +> same edit, so the declared rule says what the doors enforce. ⛔ A note without that narrowing +> would have left a fresh declared ≠ enforced — the defect class ADR-0049 exists to refuse. +> +> **What is NOT decided here.** Making the key a principal category of its own — key-held grants, +> a key-scoped audit subject — was considered and refused: zero measured pull, and a principal +> category is hard to retire once granted. If a compliance requirement to tell credential-driven +> writes from hand-driven ones is ever stated, that is a new decision with the requirement named, +> ⛔ not a reading of this one. +> +> Ruling: director seat, batch #139 item 1, maintainer 「同意」 2026-09-16 — letter **A**. + ### D11 — OWD gains an external dimension (`externalSharingModel`) Portal and partner scenarios need the Salesforce insight: internal and external record baselines From b0bf1ae280b8c64e839f2ffde057e01231adf631 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 11:42:17 +0000 Subject: [PATCH 2/3] docs(adr): name all three API-key doors and the guard that does the work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The note's Why paragraph anchored the REST door and called the exclusion structural. Both halves misdescribed the tree. A key is honoured on every door that runs the shared resolver — REST, the runtime / MCP HTTP dispatcher and the MCP stdio transport. REST and stdio pass `oauth: undefined` by construction; on the HTTP dispatcher, the door that does mint agent principals, the exclusion is a two-line guard that refuses an `osk_` bearer and anything that is not a three-segment JWS. Name the guard, and say that loosening it would break this decision silently. A right conclusion reached through a wrong mechanism is a fresh declared ≠ enforced inside the sentence meant to close one. Claude-Session: https://claude.ai/code/session_01HnRAeVTLJevtQ5iCPX6JSm Co-authored-by: Claude --- ...90-permission-model-v2-concept-convergence.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/adr/0090-permission-model-v2-concept-convergence.md b/docs/adr/0090-permission-model-v2-concept-convergence.md index 51630d5e41e..a27d9806788 100644 --- a/docs/adr/0090-permission-model-v2-concept-convergence.md +++ b/docs/adr/0090-permission-model-v2-concept-convergence.md @@ -417,9 +417,19 @@ alias tax on every API. > different path — `packages/core/src/security/api-key.ts#resolveApiKeyAdmission` into > `packages/core/src/security/resolve-authz-context.ts#resolveAuthzContext` — which resolves the > key's OWNER and nothing else. The assembled principal is therefore `human`, carries neither -> `onBehalfOf` nor `performedBy`, and its `sys_audit_log` row is the owner's own. The REST door, -> which is where keys are honoured, hands the assembler no OAuth provenance at all, so an agent -> principal is not representable there by construction. +> `onBehalfOf` nor `performedBy`, and its `sys_audit_log` row is the owner's own. A key is honoured +> on **every** door that runs +> `packages/core/src/security/resolve-authz-context.ts#resolveAuthzContext` — REST, the runtime / +> MCP **HTTP** dispatcher, and the MCP **stdio** transport — and none of the three turns one into +> an agent, by two different mechanisms. REST and stdio hand the assembler +> `oauth: undefined` **by construction** (`packages/rest/src/rest-server.ts`, +> `packages/mcp/src/plugin.ts#resolveStdioExecutionContext`), so agent provenance is not +> representable on either. On the HTTP dispatcher — the one door that does mint agent principals — +> it is a **guard** rather than a structural impossibility: +> `packages/runtime/src/security/resolve-execution-context.ts#extractJwtBearer` refuses an +> `osk_`-prefixed bearer and anything that is not a three-segment JWS, so no OAuth provenance is +> ever derived from a key. ⚠️ That guard IS this decision's enforcement on that door: loosening it +> would make an API key an agent principal without anyone editing this record. > > **What this changes.** Nothing in the runtime — the behaviour above IS the decided behaviour. > What changes is the DECLARATION: rule 4's 「every write」 is narrowed to agent principals in the From e88ee1d5ffd43bd02b7bab4e70d76ecc86030559 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 12:07:23 +0000 Subject: [PATCH 3/3] docs(adr): drop the counterfactual attached to the JWT-bearer guard The sentence claimed the guard IS this decision's enforcement on the MCP HTTP door and that loosening it alone would make an API key an agent principal. Both halves are false of the tree. `verifyMcpAccessToken` refuses a non-JWS token on its own first line and then checks signature, issuer, audience, `sub` and `azp`; `extractApiKey` reads `x-api-key` and the `ApiKey` scheme before any bearer, so a key presented either way never meets the guard at all; and the assembler mints an agent from `oauth.clientId` alone. The paragraph is correct and complete without it: it names the three doors that feed the assembler, keeps "by construction" only where it holds, and describes the guard as a guard. The counterfactual was never part of the ruled deliverable, so it is deleted rather than rewritten a third time. Claude-Session: https://claude.ai/code/session_01HnRAeVTLJevtQ5iCPX6JSm Co-authored-by: Claude --- docs/adr/0090-permission-model-v2-concept-convergence.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/adr/0090-permission-model-v2-concept-convergence.md b/docs/adr/0090-permission-model-v2-concept-convergence.md index a27d9806788..ffe99c0d9bb 100644 --- a/docs/adr/0090-permission-model-v2-concept-convergence.md +++ b/docs/adr/0090-permission-model-v2-concept-convergence.md @@ -428,8 +428,7 @@ alias tax on every API. > it is a **guard** rather than a structural impossibility: > `packages/runtime/src/security/resolve-execution-context.ts#extractJwtBearer` refuses an > `osk_`-prefixed bearer and anything that is not a three-segment JWS, so no OAuth provenance is -> ever derived from a key. ⚠️ That guard IS this decision's enforcement on that door: loosening it -> would make an API key an agent principal without anyone editing this record. +> ever derived from a key. > > **What this changes.** Nothing in the runtime — the behaviour above IS the decided behaviour. > What changes is the DECLARATION: rule 4's 「every write」 is narrowed to agent principals in the