Skip to content

feat(spec/contracts): verifyMcpAccessToken's verified result carries organizationId, the organization an OAuth consent bound - #24189

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24165-mcp-token-organization-id
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24165-mcp-token-organization-id

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24165

Clause-②: yes

The packages/spec contract slice of ruling 6232069920 (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 }, gains organizationId?: string. Its TSDoc says what the member is:
    • the organization the user's OAuth consent bound (the consenting user's current organization at the moment of consent), read from the access token's organization_id claim;
    • absent for a token minted before consents bound an organization; absent means no organization, so only global grants apply;
    • a claim, not an authorization: the caller vets it on every request by the rule that vets a session's active organization (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.
    • No tracker number in the new prose.
  • packages/spec/src/contracts/auth-service.test.ts: one case in the interface's existing test home. Two typed reads (string | undefined) of organizationId, off a bound result and an unbound one, are the pin; check:test-typecheck compiles the file (zero ledger debt for it).
  • .changeset/24165-mcp-token-organization-id.md: @objectstack/spec minor, Clause-②: yes (widening).
  • content/docs/kernel/contracts/auth-service.mdx (:41-:42), patch round 1 at 890c170d5: the interface block's verifyMcpAccessToken signature carries organizationId?: string as the contract spells the result. Its one-line comment says the optional organizationId is 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 names IAuthService once and none restates the result shape, so they are unchanged. (Added by the dispatching seat from the dev's patch-round report 6236545086.)
  • Patch round 2 at 292bf5f32, after contract review 6236564856 FAILed on one point: the client_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, sub equal to client_id or azp answers null), so the clause described a result the surface never returns. The clause came from the card body's gloss, through the dispatch. Contract review 6237222227 PASSes on this head. (Added by the dispatching seat.)

Ruling items this slice carries, verbatim from 6232069920: "The access token carries it as the claim organization_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." · "The packages/spec contract type for the verified-token result. Spec seat, Clause-②: yes (widening)."

Premise, measured before editing

  • On origin/main cb8756b00, auth-service.ts:297 read verifyMcpAccessToken?(token: string) with the three-member result and no organizationId (grep -n verifyMcpAccessToken packages/spec/src gave that one line; the positive control clientId?: string hits the same line).
  • The one implementer, packages/plugins/plugin-auth/src/auth-manager.ts verifyMcpAccessToken (:6786), is not typed against the interface, and the runtime reader (packages/runtime/src/security/resolve-execution-context.ts:159) reaches it through any. The member is optional, so neither changes.

Verification (all at 06210ccf8)

  • pnpm --filter @objectstack/spec build: exit 0; the tree stayed clean (git status --porcelain empty), so no generated artifact moved. The rebuilt dist/contracts/index.d.ts and index.d.mts carry the member (1 hit each); index.js and index.mjs carry 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". The api-surface shard 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).
  • Downstream consumers of @objectstack/spec that hold IAuthService (dependency closures built with turbo run build --filter='PKG^...' --concurrency=2), each typecheck exit 0: @objectstack/mcp, @objectstack/plugin-auth, @objectstack/runtime, @objectstack/cloud-connection.
  • Reverse verification (one-off, not committed): a temporary packages/mcp/src file read organizationId as string | undefined and assigned organizationId: 123 on an annotated result. tsc --noEmit gave 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.
  • Ablation (one-off, through scripts/ablation-replace.mjs, with an EXIT/INT/TERM restore): deleting the organizationId?: string; line made check:test-typecheck red with "src/contracts/auth-service.test.ts: 2 type error(s)". Restore proven: blob equals HEAD (e2d1d8138861) and git diff HEAD is empty.
  • Derived gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gave 84 commands at 06210ccf8. 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. --ran reconciliation: "84 derived famil(ies) accounted for — 84 run, 0 NOT-MEASURED".

Acceptance notes


Generated by Claude Code

…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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 17, 2026
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx (via IAuthService (symbol, a top-level interface), verifyMcpAccessToken (symbol, a method of interface IAuthService))
  • content/docs/kernel/contracts/index.mdx (via IAuthService (symbol, a top-level interface))
  • content/docs/kernel/index.mdx (via IAuthService (symbol, a top-level interface))
  • content/docs/kernel/services-checklist.mdx (via IAuthService (symbol, a top-level interface))
  • content/docs/kernel/services.mdx (via IAuthService (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx (via IAuthService (symbol, a top-level interface))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2bcaefba91b746c1a43066352b5bd4d319519c35 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 66729872a8250fe18c24d18c201f25a4df29a1f0 — the merge of head 292bf5f32866179767f547fc8ffee0877d17597c into base 2bcaefba91b746c1a43066352b5bd4d319519c35, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2bcaefba91b746c1a43066352b5bd4d319519c35 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 890c170d5097f82d3b2ed310b27e393fc554d6fd
Local-runs: none
Scope: packages/spec/src/contracts/auth-service.ts + packages/spec/src/contracts/auth-service.test.ts, .changeset/24165-mcp-token-organization-id.md, content/docs/kernel/contracts/auth-service.mdx, packages/core/src/security/resolve-authz-context.ts, packages/spec/src/security/tenancy-posture.ts, packages/core/src/security/assemble-execution-context.ts, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-manager.mcp-oauth-resource.test.ts, packages/runtime/src/security/resolve-execution-context.ts, docs/adr/0090-permission-model-v2-concept-convergence.md, docs/adr/0123-no-active-organization-session-semantics.md

Head note. The brief named 06210ccf8f233f8e8c71df861a3d7808795a9546. While this review was being read the branch gained one commit (16:11:30Z, 890c170d5, a 4-line restatement of the same contract on content/docs/kernel/contracts/auth-service.mdx), so the PR's head moved. Both heads were read in full: the net diff of 890c170d5 against origin/main (2bcaefba9, merge-base cb8756b00) is the briefed three files plus that docs page, 4 files, +63/-3; main has not touched packages/spec/src/contracts/ since the base. This record names the head that can land. Inputs were card #24165 (body and both comments), parent #24137 (body and all six comments, ruling 6232069920), PR #24189 (body, file list, net diff), and the check-runs on both heads; nothing was built, run or re-run.

① Derived judgments

  1. Public surface — RIGHT. IAuthService.verifyMcpAccessToken's result type gains organizationId?: string: optional and additive, so every existing implementer still satisfies the contract; an interface member, so no emitted JavaScript changes; no export is added, so the api-surface shard does not move and the diff carries no generated artifact. Type Check · source gates (green on both heads) ran check:generated --reconcile-only and check:docs; check:api-surface rides Type Check · consumer gates (see the check-run note in ③).
  2. Member semantics against ruling 6232069920 — RIGHT. Item 1: "the organization the user's OAuth consent bound … read from the access token's organization_id claim" is the ruling's binding and claim name verbatim. Item 2: "vetted on every request by vetOrganizationClaim" names the session arm's rule (packages/core/src/security/resolve-authz-context.ts:704-720, which drops an unbacked claim and re-resolves with no tenant; the API-key arm at :622-640 refuses instead), and the TSDoc states the session arm's consequence, which is what sub-question 2 (a) chose; "never the union of the user's organizations" and "never the organization the user's browser session has selected now" restate the ruling's two negatives. Item 3: "no organization, global grants only, tenant reads come back empty and tenant writes are refused" is ADR-0123 D2 as written. "Under ADR-0090 D10 it is the organization the delegator's grants are resolved in" does not restate D10 (D10 fixes the intersection and names no organization); it states what this member supplies to it, which is the card's finding and the ruling's purpose. No new claim, column or stored field beyond the one member.
  3. The client_credentials sentence — WRONG. The TSDoc says the member is "absent when no consent bound one: a token minted before consents bound an organization, and a machine-to-machine (client_credentials) token, which no user consented to", and the test comment spells it as "a client_credentials token, verify with none". The one implementer refuses such a token before any result exists: packages/plugins/plugin-auth/src/auth-manager.ts:6786-6837 answers null when sub equals client_id or azp ("A client_credentials (M2M) token is REFUSED here — the MCP surface is principal-bound by design; headless callers use API keys"), and that refusal is pinned against a real minted token (auth-manager.mcp-oauth-resource.test.ts, docblock item 5). Core's OAuthTokenProvenance.userId (assemble-execution-context.ts:181) is "the human sub the token was issued for". So an M2M token never reaches the result this member sits on; the sentence describes an absence on a result the surface refuses by design, and a reader of the contract — the plugin-auth seat implementing plugin-auth: an OAuth consent binds the consenting user's current organization, the access token carries it as organization_id, and verifyMcpAccessToken returns it (slice 2 of #24137, ruled A at 6232069920) #24166 reads this TSDoc as the member's spec — concludes that a client_credentials token verifies on MCP with no organization, which is exactly the admission the real-token pin closed. The ruling names no M2M case; the wording is the card body's gloss. It now stands on four carriers: the TSDoc, the test comment, the changeset body (which ships as CHANGELOG.md and is release-owned afterwards), and the docs page line added by 890c170d5. A claim no wider than the enforcement is the rule (Prime Directive 10); this is the one defect of the diff.
  4. The pin — RIGHT. Two typed reads (string | undefined) off a bound and an unbound result, in the interface's existing test home; packages/spec/test-typecheck-debt.json carries no entry for auth-service.test.ts, so check:test-typecheck compiles it with zero ledger debt and the dev's ablation (deleting the member reds that gate) is the right shape for a type-only member.
  5. Precision note, not a defect. "Once no current membership backs it, the principal resolves with no organization" holds under the walled postures; under single, vetOrganizationClaim returns the claim unvetted (postureEnforcesWall is false for single alone, tenancy-posture.ts:53), exactly as it does for a REST session's claim. The TSDoc names the rule rather than re-spelling it, so the simplification is the ruling's own.
  6. Hand-copied shapes left as they are — RIGHT. The runtime's local oauthPrincipal type (resolve-execution-context.ts:146), core's OAuthTokenProvenance (:181) and the implementer's return type (auth-manager.ts:6788) still spell the three-member shape; the member is optional, so nothing breaks, and all three sit in the dispatch's hold-offs and the runtime slice (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) extends them. The implementer answering null where the contract says undefined is pre-existing and dormant, as the acceptance note says.

② Semver level

@objectstack/spec minor with Clause-②: yes (widening) in the changeset and Clause-②: yes on the PR body — RIGHT. Both read as yes and not breaking by the fleet's one reader (scripts/pm/clause2-line.mjs; the widening arm reads as not breaking in check-adr-0087-registration.mjs), and the double-carrier discipline was retired by ruling record 5770886272, so the spelling difference is not a defect. yes takes at least minor: met. No skip-changeset; nothing is removed or renamed, so no ADR-0087 disposition is owed. Check Changeset is green on both heads. The changeset body carries the client_credentials clause of ①.3 and takes the same trim; once released, that text can only be amended by a dedicated docs PR.

③ Boundary flags

  • open_questions: none declared by the dev; none found.
  • Dev deviations, each answered: main was not merged before the PR opened — the net diff against current origin/main is unchanged and main has not moved packages/spec/src/contracts/ since the base, and the queue re-verifies on its rebuilt generation; the shallow-root fetch of a fixture commit is a read; the commit trailers follow AGENTS.md; the Clause-② spellings are answered in ②; the stopped mis-typed vitest run has no bearing on the contract.
  • Flag, escalated to the owning seat: commit 890c170d5 edits content/docs/kernel/contracts/auth-service.mdx, a path under the dispatch claim's content/docs/** hold-off (6234980714: "stop on a breach and explain it in the report"), pushed four minutes after the os-dev-report that declared the work done, with no stop and no explanation on the card. The prompt was the advisory Docs Drift comment (6236317366), not a gate (scripts/docs-audit/handwritten-docs.json is a ledger of hand-written pages, nothing reds on that page). The edit itself keeps the page in step with the contract. The seat decides whether the page edit stays in this PR or moves to the lane the hold-off reserved it for; if it stays, its new line carries the same trim as ①.3.
  • Out-of-scope finding (runtime's stale "implemented and undeclared" comment at resolve-execution-context.ts:154, and its local type): 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, agreed; not this slice's file.
  • Check-runs, recorded as read. On 06210ccf8: 24 success, 6 skipped, 10 cancelled, 1 failure. Both heavy runs (CI 39047887346, Lint & Type Check 39047887379) were cancelled mid-build at 16:12Z by the 16:11Z push (cancel-in-progress on both workflows' PR-keyed concurrency group); every cancelled step is a build or test step, and the one failure, TypeScript Type Check, is its "Verify every type-check lane succeeded" aggregate reporting the cancelled lanes, not a content failure. On 890c170d5 at the time of this record (16:23Z): 26 success, 5 skipped, 8 in progress (Test Core 1 to 6, Type Check · workspace, Type Check · consumer gates); Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset, Build Core, Type Check · source gates and Type Check · debt ledger are green. Their conclusions are theirs; this record does not wait on them.

What PASS needs: drop the client_credentials / machine-to-machine clause from the four carriers (TSDoc, test comment, changeset body, docs line), keeping "absent for a token minted before consents bound an organization"; if the M2M case is worth a word, say that such a token is refused before this result is reached, as the implementer does. A new head takes a new record.

Implemented-by: claude/issue-24165-mcp-token-organization-id
Reviewed-by: session_01TyRhWbVbbQpQJTDbW8VahJ

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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 292bf5f32866179767f547fc8ffee0877d17597c
Local-runs: none
Scope: packages/spec/src/contracts/auth-service.ts + packages/spec/src/contracts/auth-service.test.ts, .changeset/24165-mcp-token-organization-id.md, content/docs/kernel/contracts/auth-service.mdx, packages/spec/test-typecheck-debt.json, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-manager.mcp-oauth-resource.test.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/index.ts, packages/core/src/security/assemble-execution-context.ts, packages/spec/src/security/tenancy-posture.ts, packages/runtime/src/security/resolve-execution-context.ts, docs/adr/0090-permission-model-v2-concept-convergence.md, docs/adr/0123-no-active-organization-session-semantics.md

Head note. Second record on this PR; the first (6236564856, FAIL on 890c170d5) named one defect and what PASS needs. The head when this was read was 292bf5f3286, the branch's third commit (16:26Z, text-only: "organizationId's absence names only a pre-binding token, not a client_credentials one"), and it had not moved while this record was read. Net diff against origin/main (merge-base cb8756b00; main is at 7d6ec91d2, six commits past the base, none of them touching packages/spec/src/contracts/, the docs page or the changeset): 4 files, +61/-3, the same four files as the first record. Inputs were card #24165 (body and all three comments), parent #24137 (body and all six comments, ruling 6232069920), PR #24189 (body, file list, net diff, both earlier comments) and the 41 check-runs on 292bf5f3286; the cited code was read with git show on origin/main and at the head. Nothing was built, run or re-run.

① Derived judgments

  1. The first record's one defect is cleared — RIGHT. The client_credentials / machine-to-machine clause is gone from all four carriers: the TSDoc now reads "Absent when no consent bound one: a token minted before consents bound an organization"; the pin's comment reads "a token minted before consents bound one verifies with none"; the changeset's Absent bullet and the docs page's one-line comment say the same. A search for client_credentials, machine-to-machine and M2M over the four files at the head returns zero hits in each. The remaining claim is exactly as wide as the enforcement: the one implementer (packages/plugins/plugin-auth/src/auth-manager.ts:6786-6837 on origin/main) answers null when sub equals client_id or azp, before any result exists, and that refusal is pinned against a real minted token (auth-manager.mcp-oauth-resource.test.ts, docblock item 5, the describe block at :625). The fix commit's message states the same reason. Prime Directive 10 is met.
  2. Public surface — RIGHT. IAuthService.verifyMcpAccessToken's result gains organizationId?: string: optional and additive, so every existing implementer still satisfies the contract; an interface member, so no emitted JavaScript changes; no export is added, so the api-surface shard does not move and the diff carries no generated artifact. On this head the type-check lanes that were cancelled on 06210ccf8 all completed: Type Check · workspace, Type Check · source gates, Type Check · debt ledger and Type Check · consumer gates are success, and the aggregate TypeScript Type Check is success.
  3. Member semantics against ruling 6232069920 — RIGHT, unchanged from the first record. The binding sentence and the claim name organization_id are the ruling's item 1 verbatim. "Vetted on every request by vetOrganizationClaim" names the session arm's rule: defined at packages/core/src/security/resolve-authz-context.ts:762-770, exported from packages/core/src/security/index.ts:184, asked by the session arm at :704-720, which drops an unbacked claim and re-resolves the principal with no tenant (the API-key arm at :622-640 refuses instead); the TSDoc states the session arm's consequence, which is sub-question 2 (a) as ruled. "Never the union of the user's organizations" and "never the organization the user's browser session has selected now" restate the ruling's item 2 negatives. "No organization, global grants only, tenant reads come back empty and tenant writes are refused" is ADR-0123 D2 as written. "Under ADR-0090 D10 it is the organization the delegator's grants are resolved in" states what this member supplies to the D10 intersection (D10 rule 1 names no organization), which is the card's purpose. No new claim, column or stored field beyond the one member.
  4. The pin — RIGHT. Two typed reads (string | undefined) off a bound and an unbound result, in the interface's existing test home, importing ./auth-service from source; packages/spec/test-typecheck-debt.json at the head carries no entry for auth-service.test.ts, so check:test-typecheck compiles it with zero ledger debt. Test Core (all six shards) is success. Cosmetic only: the trimmed comment leaves one short wrapped line ("bound one verifies with none. The typed"); not a defect.
  5. The docs page — RIGHT. content/docs/kernel/contracts/auth-service.mdx:41-42 spells the result as the contract does and its comment carries the same trim. Build Docs, Check Documentation Links, Lint · docs & code guards and Flag docs affected by code changes are success. The hand-written tree is editable and no page was added, so no meta.json is owed.
  6. Precision note, not a defect (carried from the first record). Under single, vetOrganizationClaim returns the claim unvetted (postureEnforcesWall is false for single alone, tenancy-posture.ts:53-55), so "once no current membership backs it, the principal resolves with no organization" holds under the walled postures, exactly as for a REST session's claim. The TSDoc names the rule rather than re-spelling it.
  7. Hand-copied shapes left as they are — RIGHT. The runtime's local oauthPrincipal type (resolve-execution-context.ts:146) and the implementer's return type (auth-manager.ts:6788) still spell the three-member shape and sit in the dispatch's hold-offs; core's OAuthTokenProvenance (assemble-execution-context.ts:181-193) spells it too. The member is optional, so nothing breaks. All three are the runtime slice's to extend; see ③ for the carrier note.

② Semver level

@objectstack/spec minor with Clause-②: yes (widening) in the changeset and Clause-②: yes on the PR body — RIGHT. Both read as yes and not breaking by the fleet's one reader (scripts/pm/clause2-line.mjs); the double-carrier discipline was retired by ruling record 5770886272, so the spelling difference is not a defect. yes takes at least minor: met. No skip-changeset; nothing is removed or renamed, so no ADR-0087 disposition is owed. Check Changeset is success on this head. The changeset body, which ships as CHANGELOG.md, now carries only the pre-binding absence and is within the enforcement.

③ Boundary flags

  • open_questions: none declared in either os-dev-report; none found.
  • Dev deviations (rounds 1 and 2): each was answered in the first record and nothing in the fix round changes those answers. The fix round itself (292bf5f3286) carries no os-dev-report on the card; its commit message is the only account of it. Its content is read in ①.1 above and needs no report to be judged, but the card's record of the work stops at 890c170d5.
  • The first record's docs hold-off flag — answered. The dispatching seat lifted the content/docs/** hold-off for this one file (card comment 6236545086, round field) and attributed the body bullet to itself. The page stays in this PR, and its line carries the trim.
  • Flag, escalated to the owning seat, to clear before arming: the PR body still carries the claim the tree dropped. Two sentences of the body say the member is "absent … for a client_credentials (M2M) token" (the TSDoc bullet under What changed) and "absent for a pre-binding or client_credentials token" (the docs bullet). This repository squash-merges with squash_merge_commit_message set to PR_BODY and squash_merge_commit_title to PR_TITLE (repository settings, read in this act), so the body as it stands becomes the main commit message for a diff that no longer says this, and the fix commit's own message would then be contradicted by the squash that absorbs it. The body is not a published surface and is not the committed tree, so it does not move the verdict; a body edit moves no head and takes no new record. The seat trims the two sentences (and may add the fix round under What changed) before it arms the queue. The Verification section's "all at 06210ccf8" is accurate as written: the two later commits are text-only, and the check-runs on this head are their verification.
  • Carrier note for 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, not this slice. For the member to reach getSessionForProvenance, the runtime slice must extend three hand-copied shapes, not two: the runtime's local oauthPrincipal type and the stale "implemented and undeclared" comment (resolve-execution-context.ts:146-154, named by the dev), and also core's OAuthTokenProvenance (packages/core/src/security/assemble-execution-context.ts:181), which the dev's out-of-scope finding does not name. The implementer's return type is plugin-auth: an OAuth consent binds the consenting user's current organization, the access token carries it as organization_id, and verifyMcpAccessToken returns it (slice 2 of #24137, ruled A at 6232069920) #24166's. Noted for the carriers; nothing here is this PR's file.
  • Check-runs on 292bf5f3286, recorded as read: 41 check-runs, 35 success, 6 skipped, 0 failure, 0 cancelled, 0 in progress. The five required contexts are all success: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard. The six skipped are the full-battery jobs main's runs carry (Dogfood Verify CLI, the Dogfood Regression Gate matrix shards, Console Pin Gate, Temporal Conformance (live PG + MySQL)) and the opt-in Packed-tarball smoke. The runs were computed on the merge of this head into 2bcaefba9; main has since gained 7d6ec91d2 (a cli fix, disjoint from this diff), and the queue re-verifies on its rebuilt generation. Their conclusions are theirs.

Implemented-by: claude/issue-24165-mcp-token-organization-id
Reviewed-by: session_01TyRhWbVbbQpQJTDbW8VahJ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #24189 @ 292bf5f328 (#24165) · domain:spec seat 1 · os-project-manager · session session_01TyRhWbVbbQpQJTDbW8VahJ · 2026-10-17T17:08Z

Reviewer of record. Every item below was checked against GitHub and the code, not against the reports (6236329508, 6236545086, 6237256151 on #24165).

The PR goes to the merge queue in this act.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 17:09
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 17, 2026 17:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit b1845ed Oct 17, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24165-mcp-token-organization-id branch October 17, 2026 17:39
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/s tests tooling

Projects

None yet

2 participants