Skip to content

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

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-28178-oauth-api-action-forward
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-28178-oauth-api-action-forward

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V

Fixes #28178

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:通过 OAuth 连接的 AI 智能体,运行一个会转发到其他接口的 api 类动作时,现在在转发之前就被明确拒绝,拒绝语句写清是哪个动作、转发到哪里、为什么不能跑、换什么方式能跑;list_actions 照样列出这些动作,但标上 unavailable: { code, reason },智能体能直接告诉用户原因。
  • 为什么改:OAuth 访问令牌只在 MCP 端点上有效,转发到数据接口或身份接口时目标认不出它,把已登录的智能体当成匿名用户,回一个「请先登录」的 401,用户和智能体都看不懂。维护者裁决(选项 B,6540286612):不放宽令牌的接受范围,改为按一张「凭据类型 × 目标接口」表在转发前点名拒绝。
  • 风险与回滚:没有放宽任何接口接受的凭据,OAuth 令牌仍只在 MCP 端点有效;会话与 API Key 能跑的转发照旧能跑。API Key 调身份接口的既有拒绝(runtime: an api action that forwards to an identity (better-auth) route fails for an API-key caller, and nothing pins it #28217)并入同一张表,语句不变。回滚即 revert 本 PR,无数据迁移。
  • 先看:packages/runtime/src/action-execution.ts(API_FORWARD_DOOR_ACCEPTS 表与 apiForwardCredentialRefusal);packages/runtime/src/domains/mcp.ts(list_actions 加标记);packages/mcp/src/mcp-http-tools.ts(新增可选成员 unavailable 与导出类型)。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-forward.test.ts,预期全绿,其中 [#28178] 一组 7 个用例覆盖表格逐格、两扇门的拒绝和 list_actions 的标记。
  • 需要你做的:无,席位落地。

What changes

An agent connected to MCP over OAuth holds an access token the platform honours on the MCP endpoint only. A same-origin type: 'api' action is forwarded with the caller's own credentials, so the forward reached every other target as nobody, and the agent got the target's anonymous 401 ("Sign in first") although it had authenticated. Ruling B (6540286612) on this card: refuse by name before the forward, from one table, and move no boundary.

  • One table, credential kind × target door (API_FORWARD_DOOR_ACCEPTS in packages/runtime/src/action-execution.ts): an identity route (/api/v1/auth/…) accepts a session only; the MCP endpoint a session, an API key or an OAuth token; every other same-origin target a session or an API key. apiForwardCredentialKind reads the admitted credential (oauth when the execution context carries oauthScopes, else the door's admission.credential); apiForwardCredentialRefusal reads the table.
  • Both run doors refuse before the forward. forwardApiAction (shared by MCP run_action and REST POST /actions/:object/:action) answers 401 UNAUTHENTICATED with a sentence naming the action, its declared target and what would let it run. The target is never called.
  • list_actions marks, it does not hide. buildMcpBridge.listActions reads the same predicate and adds unavailable: { code, reason } with the same code and sentence run_action would answer. McpActionSummary gains the optional unavailable member and @objectstack/mcp exports the McpActionUnavailable type; the list_actions tool description says what the mark means.
  • The API-key lane (runtime: an api action that forwards to an identity (better-auth) route fails for an API-key caller, and nothing pins it #28217) is now one cell of the table (api_key × identity): its sentence is unchanged, and the listing marks it for an API-key caller too.
  • Unchanged: where an OAuth access token is accepted (the MCP endpoint only; the dispatcher's acceptOAuthAccessToken is not touched), which credentials any target door accepts, and every forward the table allows.
  • Docs: content/docs/ai/actions-as-tools.mdx (the list_actions row and the api row). Changeset: @objectstack/runtime and @objectstack/mcp, patch (Metadata-protocol: none).

One-PR exception. The card was relabelled to domain:spec on triage's retriage answer because the MCP listing's exported shape widens; the packages/mcp half (index.ts, mcp-http-tools.ts) and the runtime table ride this one PR under that answer's declared file surface. No packages/spec file is touched.

Tests

  • packages/runtime/src/domains/api-action-forward.test.ts, new [#28178] block: the table cell by cell; MCP run_action for an OAuth agent on a data-door-style target and on an identity-route target (named 401 UNAUTHENTICATED, nothing forwarded); the REST door's shared forward refusing the same caller; session and API-key callers of the same action still forwarded; list_actions marking every api action for the OAuth agent, nothing for a session, the identity-route action only for an API key. The [#28217] block passes unchanged on the moved cell.
  • packages/qa/dogfood/test/mcp-oauth-api-action-forward.dogfood.test.ts: over a real OAuth access token on the booted showcase, the listing marks, run_action answers the named refusal and leaves the row untouched, the identity-route Ban action bans nobody, session and API-key lanes unchanged, and the boundary pin: the same token sent straight to the data door or an identity route is still refused.

Acceptance notes

Verification

All readings on head 373031aae6 (the branch after merging origin/main a8d2fd7f30, a clean merge; the branch delta stayed the same 9 files, +528 / -38).

Targeted suites and typecheck
  • pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-forward.test.ts: Test Files 1 passed, Tests 49 passed.
  • pnpm --filter @objectstack/runtime test (the merge brought in runtime commits, so the whole package was rerun): Test Files 493 passed, Tests 8031 passed, 19 skipped.
  • pnpm --filter @objectstack/mcp test: Test Files 47 passed, Tests 603 passed.
  • pnpm --filter @objectstack/runtime typecheck, pnpm --filter @objectstack/mcp typecheck: exit 0.
  • pnpm --filter @objectstack/dogfood exec vitest run test/mcp-oauth-api-action-forward.dogfood.test.ts: Test Files 1 passed, Tests 5 passed.
Gate families
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 91 families derived for the 9 paths; every one run, exit codes captured before any pipe.
  • 88 exited 0 on the first pass. 3 exited 3 (PREREQUISITE NOT MET, nothing measured): check:skill-examples and check:dual-build-cjs-loads (packages outside the dogfood build closure had no dist/) and check:type-check-debt (its closure build did not get a turn on the shared lock). After building those packages, and running check:type-check-debt under the lock, all three exited 0.
  • --ran reconciliation: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).
  • pnpm lint: exit 0.

… one credential × target-door table, and mark it in list_actions

An agent connected to MCP over OAuth holds an access token the platform
honours on the MCP endpoint only, so an api action's forward reached every
other target as nobody and the agent got the target's anonymous 401. Ruled B
(6540286612): one table of credential kind × target door, read by run_action,
the REST action door and list_actions. A caller whose credential the target
cannot read is refused 401 UNAUTHENTICATED before the forward with a sentence
naming the action, its target and what would let it run; list_actions keeps
the action and marks it `unavailable: { code, reason }`. The API-key
identity-route refusal is one cell of the table, its sentence unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1MkJvadehWmrLUUT1
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/mcp, @objectstack/runtime, touching 17 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/mcp/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-api.mdx (via api_key (literal, a string literal in ApiForwardCredentialKind; a string literal in apiForwardCredentialRefusal; a string literal in forwardApiAction; a string literal in mcp; a string literal in platform))
  • content/docs/api/environment-routing.mdx (via /environments/:environmentId (route, a path literal in a comment in apiForwardTargetDoor))
  • content/docs/automation/connectors.mdx (via api_key (literal, a string literal in ApiForwardCredentialKind; a string literal in apiForwardCredentialRefusal; a string literal in forwardApiAction; a string literal in mcp; a string literal in platform))
  • content/docs/concepts/architecture-overview.mdx (via /environments/:environmentId (route, a path literal in a comment in apiForwardTargetDoor))
  • content/docs/data-modeling/field-type-decision-tree.mdx (via api_key (literal, a string literal in ApiForwardCredentialKind; a string literal in apiForwardCredentialRefusal; a string literal in forwardApiAction; a string literal in mcp; a string literal in platform))
  • content/docs/data-modeling/field-types.mdx (via api_key (literal, a string literal in ApiForwardCredentialKind; a string literal in apiForwardCredentialRefusal; a string literal in forwardApiAction; a string literal in mcp; a string literal in platform))
  • content/docs/deployment/publish-and-preview.mdx (via /environments/:environmentId (route, a path literal in a comment in apiForwardTargetDoor))
  • content/docs/deployment/single-project-mode.mdx (via /environments/:environmentId (route, a path literal in a comment in apiForwardTargetDoor))
  • content/docs/permissions/authorization.mdx (via buildMcpBridge (symbol, a top-level function))
  • content/docs/protocol/kernel/config-resolution.mdx (via api_key (literal, a string literal in ApiForwardCredentialKind; a string literal in apiForwardCredentialRefusal; a string literal in forwardApiAction; a string literal in mcp; a string literal in platform))
  • content/docs/protocol/kernel/http-protocol.mdx (via /environments/:environmentId (route, a path literal in a comment in apiForwardTargetDoor))
  • content/docs/protocol/objectql/types.mdx (via api_key (literal, a string literal in ApiForwardCredentialKind; a string literal in apiForwardCredentialRefusal; a string literal in forwardApiAction; a string literal in mcp; a string literal in platform))
  • content/docs/protocol/objectui/concept.mdx (via api_key (literal, a string literal in ApiForwardCredentialKind; a string literal in apiForwardCredentialRefusal; a string literal in forwardApiAction; a string literal in mcp; a string literal in platform))
  • content/docs/ui/forms.mdx (via /environments/:environmentId (route, a path literal in a comment in apiForwardTargetDoor))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via /environments/:environmentId (route, a path literal in a comment in apiForwardTargetDoor))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/mcp/src/index.ts) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 212 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 — 35 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 5517005d3a53600300f5cb8974ae20f178b63132 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 620a4aa745f87479e7d6a3350e86b36bfeaf479a — the merge of head 373031aae6e59bcd8f78cf07faf61f3c2cd63250 into base 5517005d3a53600300f5cb8974ae20f178b63132, 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 620a4aa745f87479e7d6a3350e86b36bfeaf479a && git checkout 620a4aa745f87479e7d6a3350e86b36bfeaf479a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5517005d3a53600300f5cb8974ae20f178b63132 373031aae6e59bcd8f78cf07faf61f3c2cd63250 && git checkout -B drift-repro 5517005d3a53600300f5cb8974ae20f178b63132 && git merge --no-ff 373031aae6e59bcd8f78cf07faf61f3c2cd63250

node scripts/docs-audit/affected-docs.mjs --json 5517005d3a53600300f5cb8974ae20f178b63132

⚠️ 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 5517005d3a53600300f5cb8974ae20f178b63132 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

维护者速读(席位终稿 · head 373031aa)

  • 改了什么: 通过 OAuth 连接的 AI 智能体,运行一个要转发到其他接口的 api 动作时,现在会在转发之前被拒绝,并写明原因。list_actions 仍然列出这些动作,只是加上 unavailable: { code, reason } 标记,智能体可以直接告诉用户为什么跑不了。
  • 为什么改: OAuth 令牌只在 MCP 端点有效。以前转发出去后,目标接口不认这个令牌,把已登录的智能体当成匿名用户,回一句「请先登录」,用户和智能体都看不懂。按维护者裁决 B(6540286612):不放宽令牌的使用范围,改用一张「凭据类型 × 目标接口」表,在转发前点名拒绝。
  • 风险与回滚: 没有放宽任何接口能接受的凭据。会话和 API Key 原来能跑的照样能跑。API Key 调身份接口的既有拒绝(runtime: an api action that forwards to an identity (better-auth) route fails for an API-key caller, and nothing pins it #28217)并入同一张表,提示语不变。回退本 PR 即可,不涉及数据。
  • 先看: packages/runtime/src/action-execution.ts(API_FORWARD_DOOR_ACCEPTS 和 apiForwardCredentialRefusal);packages/runtime/src/domains/mcp.ts(列表打标记)。
  • 两分钟验证: pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-forward.test.ts,应全部通过。
  • 需要你做的: 无。Tier S,契约复审 PASS 后由席位落地。

席位意见: ACCEPT(车道部分)。一张表同时供 run_action、REST 动作入口和 list_actions 读取,列表上的标记和运行时的拒绝不会对不上。真正让 OAuth 智能体能跑这些动作,是决策卡 #28367(选项 A),不在本 PR。

席位核对 · ACCEPT (lane part) · card #28178 · 2026-10-31T16:22Z

  1. 形态: 9 files, +528 / −38 at head 373031aa (resume head 06249f4264 plus a clean merge of origin/main a8d2fd7f30), draft, Fixes #28178 once, Clause-②: yes (widening) / Metadata-protocol: none, changeset .changeset/28178-oauth-agent-api-action-forward-refusal.md (@objectstack/runtime + @objectstack/mcp patch). check-governed-merges.mjs --pr 28442: NOT governed, 0 of 9 paths on the register.
  2. 前提实测: holds. The forward carries the caller's own credentials (forwardApiAction); an OAuth access token is accepted only where the dispatcher's acceptOAuthAccessToken reads it (the MCP endpoint, plain and environment-scoped). So an OAuth caller reached a data-door or identity target as nobody. The seat read the diff: the table's platform row is session | api_key, its mcp row adds oauth, its identity row is session only; apiForwardCredentialKind reads oauthScopes on the execution context first, so an OAuth principal is never classed by the door's admission record.
  3. 门禁 check-runs: on 373031aa at this record: 14 success, 12 skipped, 21 in progress, 0 failed. The seat re-reads them before any landing stroke. Dev: 91 gate families derived, 91 run, final non-zero 0 (three first exited 3 on an unbuilt workspace or a lock turn, then 0); --ran 0 NOT-MEASURED, 0 unrun; pnpm lint 0. Runtime 8031 passed, mcp 603/603, the OAuth dogfood pin 5/5. os-dev-report 6545995242.
  4. 接受的偏差:
    • unavailable is a new optional member on a published TS shape (McpActionSummary) plus the McpActionUnavailable type export: a widening; no existing member changes value. Hence Clause-②: yes (widening) and the spec-lane routing (triage's retriage answer).
    • packages/mcp and packages/runtime ride one PR under triage's one-PR exception; no packages/spec file is touched.
    • The sentence names the DECLARED target, so the listing's mark and the run door's refusal are byte-identical; the door is decided from the forward path. For the contract reviewer: confirm the mcp door regex (/^\/api\/v1(?:\/environments\/[^/]+)?\/mcp(?:[/?]|$)/) matches the dispatcher's own OAuth reading and no wider.
  5. 范围外去向: none from the dev. Option A (a server-built forward credential) is decision card [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. Skills-docs: none owed: content/docs/ai/actions-as-tools.mdx is updated here, and no skill teaches the forward's credential rule.
  6. 落地条款: Tier S path. Owed before any landing stroke: green CI on the landing head; a same-shape CONTRACT_REVIEW_TIER PASS (a published-surface widening); origin/main merged if a sibling lands on these 9 paths. This seat cannot serve that tier now and hands the review over below.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 373031a

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 373031aae6e59bcd8f78cf07faf61f3c2cd63250 · 2026-10-31T16:47Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 373031aae6e59bcd8f78cf07faf61f3c2cd63250
Local-runs: none
Scope: packages/runtime/src/action-execution.ts (API_FORWARD_DOOR_ACCEPTS, apiForwardTargetDoor, apiForwardCredentialKind, apiForwardCredentialRefusal, apiActionOAuthForwardRefusal; forwardApiAction refuses from the table, mark.target added), domains/actions.ts and the business-action dispatch (pass target), domains/mcp.ts (list_actions adds unavailable), packages/mcp mcp-http-tools.ts (McpActionSummary.unavailable?, McpActionUnavailable, tool description) + index.ts export, content/docs/ai/actions-as-tools.mdx, the changeset (@objectstack/runtime + @objectstack/mcp patch), api-action-forward.test.ts [#28178] block, the OAuth dogfood test. 9 files, +528/−38. Fixes #28178 (ruling B 6540286612; seat hand-off 6546016773 with ACCEPT (lane part) and the triage CONTRACT_REVIEW_TIER hand-off).
Inputs: card #28178 ruling record, the seat's hand-off (its five checks, incl. the regex question), the PR body, the PR diff at this head (every non-test hunk read), git grep on origin/main (resolveRequestScope's acceptOAuthAccessToken regex, isIdentityRouteTarget, isSameOriginApiTarget, oauthScopes on the execution context), check-runs (read 2026-10-31T16:52Z: all 50 latest-per-name check-runs completed, 36 success + 14 skipped, none failed, none running) — 50 checks. Nothing built or run here; the seat's numbers (runtime 493 files / 8031, mcp 603, dogfood 5/5, 91 gate families) are the dev report's.

① Derived judgments

  • One table, three readers. API_FORWARD_DOOR_ACCEPTS: identity → {session}, mcp → {session, api_key, oauth}, platform → {session, api_key}. apiForwardCredentialRefusal is read by forwardApiAction (shared by MCP run_action and REST POST /actions/:object/:action, both now passing the declared target) and by buildMcpBridge.listActions, so the listing's unavailable.reason and the run door's 401 message are the same sentence from the same call. The forward is never sent on a refusal (returns before inject).
  • The regex question (hand-off item 4) — answered: same door, no wider. The dispatcher's OAuth acceptance is /^(?:\/environments\/[^/]+)?\/mcp(?:[/?]|$)/ on cleanPath (http-dispatcher.ts L753, the path after the /api/v1 prefix); the PR's apiForwardTargetDoor uses /^\/api\/v1(?:\/environments\/[^/]+)?\/mcp(?:[/?]|$)/ on the forward's full same-origin path — the same shape with the prefix spelled in. isIdentityRouteTarget is main's existing /\/api\/v1\/auth\// test (L1420), unchanged. A target that is not same-origin (isSameOriginApiTarget, main L1131) is not judged by the table, as before.
  • Credential kind is read from the execution context first. oauthScopes is set by assembleExecutionContext only for an OAuth principal (core L388, spec execution-context.zod.ts L595), so an OAuth agent is classed oauth regardless of the door's admission record; otherwise admission.credential (session / api_key). With no kind (unauthenticated contexts) the table is not consulted and the pre-change path runs.
  • runtime: an api action that forwards to an identity (better-auth) route fails for an API-key caller, and nothing pins it #28217 folded, sentence unchanged. The former inline api_key && isIdentityRouteTarget refusal is now the api_key × identity cell; the message still comes from apiActionIdentityRouteKeyRefusal, and the [#28217] block passes unchanged. The OAuth sentences name the action, the declared target, that the token is honoured on the MCP endpoint only, and what would let it run.
  • Declarations. Clause-②: yes (widening) is right for the one published-shape change: McpActionSummary gains the optional unavailable member and @objectstack/mcp exports McpActionUnavailable; no existing member changes. Metadata-protocol: none — no served metadata key. patch on both packages matches the "fix" face; the mcp widening is additive and optional. No packages/spec path; triage's one-PR exception lets the mcp and runtime halves ride together.
  • Boundary unchanged, as ruled. acceptOAuthAccessToken is not touched; no target door accepts a new credential; every forward the table allows ran before and runs now. The fail-closed anonymous 401 from the target is replaced by a named 401 from the action door — same status, earlier and explained.

② Rulings cited

③ Narrowing / widening

④ Behaviour / flags

Implemented-by: claude/issue-28178-oauth-api-action-forward
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz
VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Landing stroke · 2026-10-31T16:56Z · head 373031aae6e59bcd8f78cf07faf61f3c2cd63250.

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/l tests tooling

Projects

None yet

1 participant