Repository navigation
feat(spec/contracts): verifyMcpAccessToken's verified result carries organizationId, the organization an OAuth consent bound - #24189
Conversation
…the consent-bound organizationId
IAuthService.verifyMcpAccessToken resolves to { userId, scopes, clientId? }.
It gains an optional organizationId: the organization the user's OAuth
consent bound, read from the access token's organization_id claim. It is
absent for a token minted before consents bound one and for a
client_credentials token. The TSDoc states it is a claim, not an
authorization: the caller vets it on every request by the rule that vets a
session's active organization, and an ended membership means no
organization (global grants only).
Type only and optional, so the one implementer and every reader are
unchanged. A typed read in auth-service.test.ts pins the member.
Claude-Session: https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 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 — 143 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 66729872a8250fe18c24d18c201f25a4df29a1f0 && git checkout 66729872a8250fe18c24d18c201f25a4df29a1f0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2bcaefba91b746c1a43066352b5bd4d319519c35 292bf5f32866179767f547fc8ffee0877d17597c && git checkout -B drift-repro 2bcaefba91b746c1a43066352b5bd4d319519c35 && git merge --no-ff 292bf5f32866179767f547fc8ffee0877d17597c
node scripts/docs-audit/affected-docs.mjs --json 2bcaefba91b746c1a43066352b5bd4d319519c35
|
…oken's result with organizationId content/docs/kernel/contracts/auth-service.mdx restated the old three-member result of verifyMcpAccessToken, which the contract change on this branch made stale. The signature now carries organizationId?: string as the contract spells it, and the one-line comment says it is the organization the user's OAuth consent bound, absent for a pre-binding or client_credentials token. Claude-Session: https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Head note. The brief named ① Derived judgments
② Semver level
③ Boundary flags
What PASS needs: drop the Implemented-by: VERDICT: FAIL Generated by Claude Code |
…g token, not a client_credentials one The organizationId TSDoc, the pin's comment, the changeset and the IAuthService docs page each said the member is absent for a client_credentials (machine-to-machine) token. That is false: the one implementer refuses such a token outright (sub equal to client_id or azp answers null, pinned against a real minted token), so it never reaches this result. The four carriers now say only that the member is absent when no consent bound one, a token minted before consents bound an organization. Text only; the type is unchanged. Claude-Session: https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Head note. Second record on this PR; the first ( ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · PR #24189 @
|
Fixes #24165
Clause-②: yes
The
packages/speccontract slice of ruling6232069920(letter A, the maintainer's 「同意」), recorded on #24137. #24137 remains open, and #24166 remains open: this PR declares the member only.What changed
packages/spec/src/contracts/auth-service.ts:IAuthService.verifyMcpAccessToken's verified result,{ userId: string; scopes: string[]; clientId?: string }, gainsorganizationId?: string. Its TSDoc says what the member is:organization_idclaim;vetOrganizationClaim,@objectstack/core); once no current membership backs it, the principal resolves with no organization (global grants only: tenant reads come back empty, tenant writes are refused), never in another one; never the union of the user's organizations, never the browser session's currently selected organization; under ADR-0090 D10 it is the organization the delegator's grants are resolved in.packages/spec/src/contracts/auth-service.test.ts: one case in the interface's existing test home. Two typed reads (string | undefined) oforganizationId, off a bound result and an unbound one, are the pin;check:test-typecheckcompiles the file (zero ledger debt for it)..changeset/24165-mcp-token-organization-id.md:@objectstack/specminor,Clause-②: yes (widening).content/docs/kernel/contracts/auth-service.mdx(:41-:42), patch round 1 at890c170d5: the interface block'sverifyMcpAccessTokensignature carriesorganizationId?: stringas the contract spells the result. Its one-line comment says the optionalorganizationIdis the organization the user's OAuth consent bound, absent for a token minted before consents bound an organization. The docs-drift check named five other pages (kernel/contracts/index.mdx,kernel/index.mdx,kernel/services-checklist.mdx,kernel/services.mdx,permissions/authentication.mdx). Each namesIAuthServiceonce and none restates the result shape, so they are unchanged. (Added by the dispatching seat from the dev's patch-round report6236545086.)292bf5f32, after contract review6236564856FAILed on one point: theclient_credentials/ machine-to-machine absence clause is removed from all four carriers (the TSDoc, the pin's comment, the changeset's Absent bullet and the docs page's comment). The one implementer refuses such a token before any result exists (auth-manager.ts,subequal toclient_idorazpanswersnull), so the clause described a result the surface never returns. The clause came from the card body's gloss, through the dispatch. Contract review6237222227PASSes on this head. (Added by the dispatching seat.)Ruling items this slice carries, verbatim from
6232069920: "The access token carries it as the claimorganization_id." · "Every request re-checks membership through the existing session path. ⛔ Never the union of the user's organizations, never a widened global grant, never the browser session's current organization (B), never a D9-style pick (C2)." · "The token falls back to no organization, so only global grants apply: reads come back empty and tenant writes are refused loudly." · "Thepackages/speccontract type for the verified-token result. Spec seat,Clause-②: yes (widening)."Premise, measured before editing
origin/maincb8756b00,auth-service.ts:297readverifyMcpAccessToken?(token: string)with the three-member result and noorganizationId(grep -n verifyMcpAccessToken packages/spec/srcgave that one line; the positive controlclientId?: stringhits the same line).packages/plugins/plugin-auth/src/auth-manager.tsverifyMcpAccessToken(:6786), is not typed against the interface, and the runtime reader (packages/runtime/src/security/resolve-execution-context.ts:159) reaches it throughany. The member is optional, so neither changes.Verification (all at
06210ccf8)pnpm --filter @objectstack/spec build: exit 0; the tree stayed clean (git status --porcelainempty), so no generated artifact moved. The rebuiltdist/contracts/index.d.tsandindex.d.mtscarry the member (1 hit each);index.jsandindex.mjscarry 0, because an interface is erased, so no emitted JavaScript byte changed.pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date". Theapi-surfaceshard does not move: it records export names, and no export was added.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 692 files passed, 20975 tests passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0 (build program, scripts program, test layer).@objectstack/specthat holdIAuthService(dependency closures built withturbo run build --filter='PKG^...' --concurrency=2), eachtypecheckexit 0:@objectstack/mcp,@objectstack/plugin-auth,@objectstack/runtime,@objectstack/cloud-connection.packages/mcp/srcfile readorganizationIdasstring | undefinedand assignedorganizationId: 123on an annotated result.tsc --noEmitgave exactly one error,TS2322: Type 'number' is not assignable to type 'string'on the second line. Against the old declaration the read would be TS2339 and the literal TS2353, so consumers read the rebuilt.d.ts. File deleted; tree clean.scripts/ablation-replace.mjs, with an EXIT/INT/TERM restore): deleting theorganizationId?: string;line madecheck:test-typecheckred with "src/contracts/auth-service.test.ts: 2 type error(s)". Restore proven: blob equals HEAD (e2d1d8138861) andgit diff HEADis empty.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackgave 84 commands at06210ccf8. All 84 ran with exit codes recorded. Four first exited 3 (prerequisites: a pinned fixture commit outside the shallow clone, and unbuilt dists). After fetching that commit and building, all four re-ran exit 0.--ranreconciliation: "84 derived famil(ies) accounted for — 84 run, 0 NOT-MEASURED".Acceptance notes
packages/runtime/src/security/resolve-execution-context.ts:154saysverifyMcpAccessTokenis "implemented (auth-manager.ts) and undeclared". That was already false onorigin/main(the contract declares the method) and stays false. It is runtime's file and the runtime slice's lane, so it is not edited here. Carrier: finding(mcp/oauth): an OAuth-token MCP agent acting for an ordinary user is denied every object operation — the D10 delegator is resolved with no active organization, so the user's organization-scoped grants do not count #24137's runtime slice, which edits that block.nullwhere the contract's type saysundefined. The contract's prose ("a falsy value") covers both, and nothing types the implementer against the interface, so it is dormant. Noted, not filed.mainmoved three commits after the base (727091c82). None touches this diff's files or any generated artifact this diff could move. CI runs on the merge ref.Generated by Claude Code