Skip to content

docs(adr): ADR-0090 D10 rule 5, the agent forward credential (step 1 of #28367) - #28455

Merged
os-zhuang merged 1 commit into
mainfrom
claude/project-thread-hem8y5
Oct 31, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/project-thread-hem8y5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Part of #28367. This is step 1 of the ruling's order, the ADR revision. The code PR is step 2, a separate change made after this lands, so the card stays open.

Clause-②: yes (widening, at the named cells of the #28178 B refusal table only)
Metadata-protocol: none

维护者速读

  • 改了什么:ADR-0090 D10 新增第 5 条 agent 规则「转发带的是 agent,不是用户」:agent 运行 api 动作时,进程内转发改带一张服务端自己签发的转发凭据,只对这一次转发有效(一个方法、一个路由、一次动作运行),一次性、短时、不可重放、出不了进程;到目标端解析成同一个 agent 主体(scope 派生的权限、onBehalfOf、performedBy)。绝不带调用方的 token 或 API key,也绝不签发人类会话(C 方案继续拒绝)。本 PR 只改 ADR,不写代码。
  • 为什么改:Tesla 在第 434 批第 4 项答「同意」(裁决记录 6545950368,go as shaped)。今天 OAuth 连接的 agent 跑任何转发到身份路由或平台路由的 api 动作都会按 runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #28178 B 表被点名拒绝,所以 ADR-0011 已开放给 agent 的身份动作实际一个都跑不通;裁决要求先在 ADR 里逐格写明哪些格子翻转,再写代码。
  • 风险与回滚:放宽只落在两格:OAuth agent × 身份路由、OAuth agent × 平台路由,后者受 scope 族规则约束(只授权读的 agent 通过动作也改不了任何状态,元数据门的写操作不翻转)。API key × 身份路由继续点名拒绝:API key 是主人自己的凭据而非 agent 主体(D10 2026-09-16 注记),给它开等于换个名字的人类会话。外部 OAuth token 在哪些门有效不变。若认为 API key × 身份路由也该翻转,那是另一项裁决。回滚即 revert 本 PR;代码尚未写,今天无运行时变化。
  • 先看:docs/adr/0090-permission-model-v2-concept-convergence.md 的 D10 末尾新增节「Amendment (2026-10-31, [Decision] auth: an agent forward credential — a server-constructed, target-bound, single-use credential so an OAuth- or API-key-connected agent can run an api action whose target is an identity route (ADR-0090 D10 amendment) #28367)」中的表格(标 FLIPS 的两格)、"No human session" 段(身份路由处 better-auth 只看到委托用户的请求级视图)、scope 族规则四条。
  • 两分钟验证:打开该文件搜 FLIPS,预期恰好两格翻转,api_key 行身份列仍为 refuse by name。
  • 需要你做的:以 os-zhuang 身份审批本 PR(Tier H,席位会发起评审请求)。

What changes

One file, docs/adr/0090-permission-model-v2-concept-convergence.md. No code and no packages/**.

  • Status block: one dated Amended (2026-10-31, ruling of record 6545950368 on #28367) line.
  • TL;DR D10 bullet: one sentence pointing to rule 5.
  • D10's agent bullet: "Four defining rules" becomes five. Rule 5 is a short statement pointing to the amendment.
  • The amendment (a dated subsection at the end of D10, after the 2026-09-20 status note):
    • The credential. Four binding properties: the server mints it after the action door's own gates pass; it is bound to one method, one route and one action run in the consent's organization; it is single-use, short-lived and non-replayable; it never leaves the process and is never stored, logged, relayed or audited.
    • What it resolves to. The same agent principal, built by entryFields from handed-over provenance: the scope-derived permissions, the existing systemPermissions rule, onBehalfOf, performedBy, oauthScopes and the consent's organization.
    • No human session. At the identity mount, better-auth gets a request-scoped in-memory view of the delegating user for that one request. There is no sys_session row, token or cookie. Two gates stand: the action door's gate (the agent's) and better-auth's own (the user's). Identity writes are attributed to the agent and the user. Credential-rotation headers are never relayed to an agent caller.
    • The table. The runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #28178 B table is reproduced with the flipped cells marked. Two cells flip: oauth × identity and oauth × platform. api_key × identity keeps refusing by name, with the reason. oauth × mcp is unchanged.
    • The scope-family rule for targets outside MCP. actions:execute is always required. A safe method needs data:read or data:write, and any other method needs data:write. A state-changing method on the metadata door stays refused. Refusals are by name before the mint, and list_actions marks the action from the same predicate.
    • Where the code half lives. packages/runtime (the action doors and the HTTP dispatcher's resolution), packages/rest (the REST face's resolution) and packages/plugins/plugin-auth (the identity mount). packages/core is unchanged. The amendment also lists the pins that PR owes.
    • What does not change, and the two sentences elsewhere in D10 that the code PR must correct when it lands.

Acceptance notes

Verification

Local, on 5e899ebd: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 21 families for the one path. Each ran with stdin from /dev/null, and its exit code was captured by redirect before any pipe.

Gate readings
  • First pass: 13 exited 0. Eight exited 3 (PREREQUISITE NOT MET, no node_modules in the fresh worktree). After pnpm install --frozen-lockfile, 7 of the 8 exit 0. check:doc-formula-expressions exited 3 again because @objectstack/formula and @objectstack/lint were not built. After turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the verify lock (VERDICT command-exit 0), it exits 0.
  • Final: all 21 exit 0, including check:adr-links, check:adr-anchors, check:adr-index, check:adr-symbol-anchors, check:doc-authoring, check:closing-keyword-parity, check:nul-bytes and check:pm-governed-merges.
  • dispatch-gates --ran: 21 derived famil(ies) accounted for — 21 run, 0 NOT-MEASURED, exit 0.
  • Outside the derivation: node scripts/check-role-word.mjs (ADR-0090 D3 ratchet) exit 0.
  • pnpm lint: no package touched, so there is no scoped lint run.

Generated by Claude Code

Amends ADR-0090 D10 with a fifth agent rule: an api action's in-process
forward carries the agent principal in a server-minted, target-bound,
single-use, short-lived credential that never leaves the process, never
the caller's token or key and never a human session. Reproduces the
credential-kind x target-door table and names the two cells that flip
(an OAuth agent at an identity route and at a platform route); the
API-key cell keeps refusing by name. Adds the scope-family rule for
targets outside MCP and says where the code half will live and what it
pins. No code.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyagSyLB5ckJsKghoa
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 31, 2026
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3

Brief-claimed-by: session_01L9qibxdiybU8ehPKBroYE3 · batch #436 · 2026-10-31T17:25Z

Director seat (the maintainer's decisions thread, 2026-10-31). The one judgement call in this PR's acceptance notes — whether api_key × identity stays "refuse by name" or flips with the two OAuth cells — is carried to the maintainer as batch #436 item 1, presented after batch #435 is answered. The ADR lands as drafted either way; a flip would be a follow-up edit. The maintainer's answer follows in a ruling comment here.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 5e899ebd5b31cd902ba31cfe584dd41b3dfad2a8 · 2026-10-31T17:47Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5e899ebd5b31cd902ba31cfe584dd41b3dfad2a8
Local-runs: none
Scope: docs/adr/0090-permission-model-v2-concept-convergence.md only: one dated status-block line, the D10 TL;DR sentence, the agent bullet's "Five defining rules" + rule 5, and the new dated subsection "Amendment (2026-10-31, #28367) — the agent forward credential (rule 5)" at the end of D10 (the credential's four properties, what it resolves to, no human session, the cell-by-cell table, the scope-family rule, where the code half lives and the pins it owes, what does not change, the ruling line). 1 file, +190/−2. Part of #28367, step 1 of ruling 6545950368 (claim 6546502251; dev report 6546700810). Tier H by path (docs/adr/**), skip-changeset.
Inputs: card #28367 body, triage 6540849042, brief-claim 6545774218, ruling 6545950368, claim and dev report, the PR body, the whole PR diff, git grep on origin/main for every symbol the amendment cites (forwardApiAction L1669, API_FORWARD_HEADER_ALLOWLIST L1166, apiActionIdentityRouteKeyRefusal L1518, API_FORWARD_RELAYED_RESPONSE_HEADERS L1190 in action-execution.ts; entryFields L304 in assemble-execution-context.ts; scopesToAgentPermissionSets L224 in mcp.zod.ts; runWithAuthActorScope L91; handleRequest in auth-manager-request-routing.ts; oauth: undefined at rest-server.ts L3443; resolve-execution-context.ts present; ADR-0011 ai.exposed / ai.requiresConfirmation), this lane's PASS on #28442 (6546352141, the #28178 B table: identity / mcp / platform doors), check-runs (read 2026-10-31T17:53Z: all 41 latest-per-name check-runs completed, 23 success + 18 skipped, none failed, none running) — 41 checks. Nothing built or run here; the seat's 21 derived gates and check-role-word are the PR body's.

① Derived judgments

  • The amendment is the ruling's shape, clause for clause. Ruling 6545950368 binds: server-minted (never the caller's bearer or key), bound to one forward (one method, one route, one action run), single-use, short-lived, non-replayable, unreachable out of process, resolving at the target to the same agent principal (scope-derived permissions, onBehalfOf, performedBy), no human session (C refused), cells named before any code, a scope-family rule for non-MCP targets. Rule 5's four properties, the "what it resolves to" paragraph, the "No human session" paragraph, the table and the four-point scope-family rule carry each of those, and nothing the ruling did not bind is decided (the agent's ceiling is unmoved; principal kinds unchanged; where an outside OAuth token is honoured unchanged).
  • The table is the runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #28178 B table with two cells flipped. oauth × identity and oauth × platform go from refuse-by-name to accept-with-forward-credential; oauth × mcp keeps the caller's own token; api_key × identity keeps refusing by name; the session row is unchanged. The flipped set is the minimum that makes ADR-0011's exposed identity actions reachable by an agent, and the scope-family rule bounds the platform flip so a system-permission-only target cannot be reached past the consented families. Metadata-door writes stay refused (MCP authoring under metadata:write is the agent's path), consistent with cloud ADR-0112.
  • The one judgement call is flagged, not hidden. The card title says "OAuth- or API-key-connected agent"; the amendment keeps api_key × identity refusing and gives the reason (D10's 2026-09-16 note: a key admits its owner, a human, so there is no agent principal to carry; carrying the owner's reach at the identity mount is option C under another name). That reading follows the ruling's own words ("resolving at the target to the same agent principal"), and the PR body and dev report put the alternative (B: flip it, carrying the key owner's principal) to the maintainer as a separate decision. Acceptable as written; the lane recommends A as the seat does.
  • Every cited symbol exists on origin/main at the stated file, so check:adr-symbol-anchors holds whichever of this PR and fix(runtime): refuse an OAuth agent's api-action forward by name from one credential × target-door table, and mark it in list_actions #28442 lands first (the table's code is named by what it decides, not by a fix(runtime): refuse an OAuth agent's api-action forward by name from one credential × target-door table, and mark it in list_actions #28442-only symbol). The two sentences the code PR must later correct (rule 4's "today the OAuth / MCP client door alone"; the 2026-09-16 note's "not representable" on the REST face) are named so they cannot rot silently.
  • Declarations. Clause-②: yes (widening, at the named cells only) — right: two refusals become acceptances once the code lands; today nothing runs differently. Metadata-protocol: none, skip-changeset: docs only. Part of #28367 keeps the card open for step 2 — correct.

② Rulings cited

  • 6545950368 (director#2, Tesla 「同意」 batch Bump all packages to v0.7.0 #434 item 4, 2026-10-31T16:12Z): go, as shaped; order ADR first, then contract review, then code — this PR is the first step. Triage 6540849042 shaped it the same way. No open ruling blocks this step; the api_key × identity question is an open item for the maintainer, not a blocker.

③ Narrowing / widening

  • Widening on paper only: oauth × identity and oauth × platform (the latter under the scope-family rule). No runtime change in this PR.

④ Behaviour / flags

  • Tier H: docs/adr/** — os-zhuang (or hotlong) approval owed before landing; the seat requests it.
  • Step 2 (the code PR on packages/runtime, packages/rest, packages/plugins/plugin-auth) owes the pin list in the amendment, each refusal ablated, and corrects the two named sentences; it gets its own contract review.
  • For the maintainer: whether api_key × identity should also flip (seat and lane: keep refusing).

Implemented-by: claude/project-thread-hem8y5
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz
VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-tesla · session_01L9qibz3ZUbZ8Qcn1rDw9yc

维护者速读

席位意见:契约复审 PASS,逐条对应裁定。可以批准。

席位核对

ACCEPT · seat domain:server#1 · head 5e899ebd · 2026-10-31T17:56Z

  1. Shape: Part of #28367 (step 1 of the ruling's order; the card stays open for the code step). Draft, base main, 1 file, +190/−2, skip-changeset. No packages/spec path.
  2. Ruling: go 6545950368. Rule 5 states the credential's four properties, what it resolves to, and that no human session exists. The table is the runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #28178 B table with two cells flipped, and a scope-family rule bounds the platform flip. Nothing the ruling did not bind is decided.
  3. Contract review: lane PASS 6547046473 at this head. It declares Clause-②: yes (widening, at the named cells only, on paper) and Metadata-protocol: none. Every cited symbol exists on main.
  4. Gates / check-runs:
    • Dev report 6546700810: 21/21 derived gates exit 0, --ran reconciled, and the role-word ratchet exits 0.
    • On head 5e899ebd: 41 check-runs completed, 23 success, 18 skipped, 0 failed.
  5. Open item, not a blocker: whether api_key × identity flips is batch Release v0.7.1 #436 item 1, carried by director#2 (6546726515).
  6. Landing: GOVERNED, Tier H (docs/adr/**). The seat readies the PR, and the relay requests os-zhuang and hotlong. No auto-merge is armed by the seat. After this lands, the seat dispatches step 2 (the code PR).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3

Ruling: batch #436 item 1 · letter A · maintainer 「同意」 2026-10-31T20:46Z

Director seat (the maintainer's decisions thread). The one judgement call in this PR's acceptance notes — whether api_key × identity flips with the two OAuth cells — was presented in batch #436 (brief-claimed here at 6546726515; detailed letter in the project's decision doc, batch 5). The maintainer answered at 2026-10-31T20:46Z, verbatim: 「同意」 = the seat's recommendation, A: the cell keeps refusing by name.

What the ruling binds: the ADR-0090 D10 amendment stands as drafted. Exactly two cells of the #28178 B table flip (oauth × identity, oauth × platform, the latter under the scope-family rule); api_key × identity stays "refuse by name", because an API key is its owner's credential and not an agent principal (D10, 2026-09-16 note), so a forward credential for it could only resolve to the owner's full reach — the human-session shape (option C) the go ruling 6545950368 excludes. Flipping that cell is a separate decision, not part of #28367. The code PR (step 2) implements the two flipped cells only.

Execution: contract review PASS 6547046473 stands; the maintainer approves this PR via os-zhuang (Tier H); the card thread lands and proceeds to step 2 with Ruling-ref: naming this comment. The card thread's own decision card on the same question is answered by this record.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants