Repository navigation
fix(runtime)!: connector connection door refuses delegated or agent callers on begin and revoke (ADR-0143 D5, ruling A) - #28474
Conversation
… begin and revoke ADR-0143 D5, ruling A (record 6548764795): connecting, reconnecting and revoking an organization's connector is a human administrator's act. The door's begin (POST) and revoke (DELETE) legs refuse a caller admitted by an API key or an OAuth access token, or carrying an agent envelope, with 403 PERMISSION_DENIED and a sentence naming its kind and the rule. The read leg, the callback, the capability holders and the system bypass are unchanged. Claude-Session: https://claude.ai/code/session_01L9qibvkQWJban49F52ZjBc Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 288e305e8217149859a0ee5ebb5c2de16d56b216 && git checkout 288e305e8217149859a0ee5ebb5c2de16d56b216
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b1dc992c7cdfffd2d4f20581fc817f18981601c 1788eefbbda8bab95b32b3c50eacc5c61795bf67 && git checkout -B drift-repro 9b1dc992c7cdfffd2d4f20581fc817f18981601c && git merge --no-ff 1788eefbbda8bab95b32b3c50eacc5c61795bf67
node scripts/docs-audit/affected-docs.mjs --json 9b1dc992c7cdfffd2d4f20581fc817f18981601c
|
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Ruling → code
② Declarations
③ Pins and ablations
④ Traps and should-fix
Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-tesla · session_01L9qibz3ZUbZ8Qcn1rDw9yc 维护者速读
席位意见:契约复审 PASS,按裁定 A 原样实现。可以直接落地。 席位核对ACCEPT · seat
|
Requested by Tesla · project thread
Fixes #28468
Clause-②: no (narrowing)
Metadata-protocol: none
维护者速读
PERMISSION_DENIED,文案说明规则:连接与撤销是人类管理员本人从登录会话里做的事。manage_connector_connections的人照旧;isSystem照旧放行;读取连接状态与 OAuth 回调两条腿不变。回滚即 revert 本 PR。packages/runtime/src/domains/automation.ts的refuseUngrantedConnectorConnectionAccess(新增按方法的拒绝分支)与connectorConnectionDelegatedCaller;automation-connector-connection-door.test.ts末尾新增的 describe 块。pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/automation-connector-connection-door.test.ts,预期 31 条全绿,其中三类代理调用方在发起与撤销上各得 403。What changes
The rule (ADR-0143 D5, ruling A, record 6548764795). The connector connection door's begin leg (
POST /api/v1/automation/connectors/:name/connection, connect and reconnect) and its revoke leg (DELETEon the same path) refuse a delegated or agent caller by name, whatever it holds. Connecting and revoking an organization's connector is a human administrator's act, taken from a signed-in session.Which callers.
connectorConnectionDelegatedCallernames three kinds:agent: an envelope that names a delegator (principalKind: 'agent'oronBehalfOf), as the shared assembler builds it for an OAuth client acting for a user;oauth: a caller admitted by an OAuth access token (the envelope carriesoauthScopes);api_key: a caller admitted by an API key (HttpProtocolContext.admission.credential).The credential half is read through
apiForwardCredentialKind(packages/runtime/src/action-execution.ts), the api-action forward table's own reading, so the two doors cannot disagree about which credential admitted a caller. A caller with no recorded credential is not named a delegate; the capability gate still judges it, fail-closed.The refusal.
403 PERMISSION_DENIED, the door's existing status and code, with a sentence fromconnectorConnectionDelegatedCallerRefusal(kind)that names the caller's kind and the rule. It runs after theisSystembypass and before the capability read, and ahead of the service probe and the query checks, like the gate it sits in.Unchanged. Who holds
manage_connector_connections(platform administrators, the organization-administrator sets, explicit grants). The system context's bypass. The read leg (GET …/:name/connection) and the OAuth callback (GET …/connectors/oauth/callback), for every caller.Docs.
content/docs/automation/connectors.mdx, the connect paragraph: connecting and revoking is a human administrator's act from a signed-in session, and an agent or an API key is refused by name.Tests
New describe block in
packages/runtime/src/domains/automation-connector-connection-door.test.ts. The envelopes are built by the realassembleExecutionContextfrom@objectstack/core, for an organization administrator holding the capability:connectorConnectionDelegatedCallernames each, and names nothing for the signed-in holder);agent,oauth,api_key: refused on begin and on revoke, 403PERMISSION_DENIED, the message equal toconnectorConnectionDelegatedCallerRefusal(kind)and naming the kind, no service member called;tenantId: 'org_a';agent,oauth,api_key: the read leg and the callback still answer 200 for a holder.File:
Test Files 1 passed (1),Tests 31 passed (31). Whole package on1788eefbb: runtimeTest Files 494 passed (494),Tests 8048 passed | 19 skipped (8067);pnpm --filter @objectstack/runtime typecheckexit 0.Ablations (one per refusal branch, plus the whole rule)
Each mutation went through
scripts/ablation-replace.mjsonpackages/runtime/src/domains/automation.ts: the anchor landed on disk (count 1 → 0), the file was restored with the blob equal to HEAD andgit diff HEADempty. The suite imports its subject fromsrc, so no dist was involved.agentbranch (principalKind === 'agent' || onBehalfOf)expected 'oauth' to be 'agent') and theagentrefusal (the sentence names an OAuth token, not an agent)oauthcredential no longer namedoauthrefusalapi_keycredential no longer namedapi_keyrefusalif (false && delegated))The first attempt at the
oauthmutation was refused by the tool itself (its replacement text was a substring of the original, so the count could not rise); it was re-run with a distinct replacement, and only that run is reported.Acceptance notes
oauthScopes, so with theagentbranch removed it is still refused, named as an OAuth token. The branch is what names an agent envelope that arrives withoutoauthScopes.content/docs/permissions/system-context.mdxrow 57c (the door'sisSystembypass) still holds: the bypass reads once in the same function, and the census count is unchanged.claude/project-thread-0wkobqis the card thread's platform-assigned branch; claim 6548795089 names the card thread's branch.Verification
Local, on
1788eefbb:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 91 families for the 4 changed paths. Each was run with stdin from/dev/null, its exit captured by redirect before any pipe: 91 exit 0.check-plugin-teardown-shape.mjs --self-testfirst exited 3 (its pinned fixture commit was not in the shallow clone); after fetching that commit it exited 0.--ran:91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED, exit 0. Scoped lint, the rootlintscript's eslint overpackages/runtimewith--no-inline-config: exit 0.pnpm --filter @objectstack/runtime typecheck: exit 0.Full dogfood (owed for the narrowing arm),
pnpm --filter @objectstack/dogfood testunder the shared lock on1788eefbb:VERDICT command-exit 0,Test Files 362 passed | 3 skipped (365),Tests 2922 passed | 28 skipped (2950).Generated by Claude Code