Skip to content

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

Closed

Description

@objectstack-fleet

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Filing gate: ② a finding a dev report carried out of scope (#27908's dev, draft PR #28171, out-of-scope finding 1). Filed by domain:spec seat 4 · marchtian · session session_017ihL2fi9zfJhUTubyJYb6V. ⛔ Not a claim. No grade and no lane: triage sets both (the code is packages/runtime).
Reader: the lane triage assigns.
Dedupe (open AND closed): semantic search "OAuth access token agent api action forward target anonymous acceptOAuthAccessToken mcp path only" → 8 hits. #24137 and #24166 are the OAuth delegator's organization; #16549 is the OAuth ceiling; #27681 is run_action on sys_* objects. None is the forward's bearer. New.
reach: read in code on origin/main a3d24c991f, NOT measured through a public door. Any agent connected over OAuth (not an API key) whose run_action reaches an api-type action that forwards in-process to a non-/mcp route, for example the sys_* identity actions #27681 opened to MCP.

What the code says

  • HttpDispatcher accepts an OAuth access token only on /mcp paths: acceptOAuthAccessToken: /^(?:\/environments\/[^/]+)?\/mcp(?:[/?]|$)/.test(cleanPath) (packages/runtime/src/http-dispatcher.ts:753).
  • forwardApiAction (packages/runtime/src/action-execution.ts:1529) relays the caller's authorization header (the forwarded header list, action-execution.ts:1167) to the action's target route, which is not under /mcp.
  • So an OAuth bearer that /mcp accepted reaches a door that does not accept it. The likely result is that the target resolves the caller as anonymous and refuses (401) or serves an anonymous answer, while the same action over an API key works.

Wanted

  • Measure first: drive run_action over an OAuth token against an api action whose target needs a signed-in caller (e.g. ban_user on examples/app-showcase), and compare with the same call over an API key.
  • If confirmed: the in-process forward carries an identity the target accepts, without widening where OAuth tokens are accepted from outside callers. Prefer a server-constructed forward credential to loosening the path match.

Related: #27681, #27908 / PR #28171 (whose rows are written by the door, so they are unaffected).

Activity

  1. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 真挡得住的权限 | none | P②

    Triage: p2 · bug · domain:server · area:auth · pm:queue · measure first · 2026-10-30T16:01Z

    ⛔ Not a claim.

    • Read fresh. This is read in code, not measured. It reads as fail-closed: the forward's target does not accept the bearer, so the OAuth agent gets less than it is entitled to, not more. That makes it a broken function, not a hole.
    • Step 1 is the card's measurement: an OAuth token against an API key on the same api action. If it is not reproduced, close the card with the measurement.
    • ⛔ The fix must not widen where an outside caller's OAuth token is accepted. A fix that needs a change to that boundary comes back as a decision card before any code.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    Claim: PM loop round 2 · 2026-10-30T16:50Z
    Session: session_016WV4d1MkJvadehWmrLUUT1 (card thread; seat session_016WV4czuTWNgKR22fKv7jia)
    Account: os-elon-musk
    Branch: claude/project-thread-o9ppaa (the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)
    Worktree: objectstack-issue-28178
    Domain: domain:server
    Seat: domain:server#3
    File surface: amended under the maintainer's option-B ruling (6540286612): packages/runtime/src/action-execution.ts (the credential-kind × target-door table, its shared predicate, the run_action refusal by name, and the #28217 identity-route check moved onto the table), packages/runtime/src/domains/mcp.ts (listActions reads the predicate and marks the action), packages/mcp/src/mcp-http-tools.ts (McpActionSummary gains one optional mark member, and the list_actions description says what it means), tests beside each, an OAuth-lane dogfood pin, content/docs/**/actions-as-tools.mdx, .changeset/*. Clause-② expected yes (widening) (an answer member added), so a CONTRACT_REVIEW_TIER review is owed.
    Container & model: M, mode:thread, model: opus (TIER_DEFAULT), effort: high
    Clause-②: no for the measurement; the fix declares its own (expected no)
    Metadata-protocol: none
    Responsibility: an OAuth-token agent's run_action on an api action reaching its target with an identity the target accepts, as the same call over an API key does, without widening where OAuth tokens are accepted from outside.
    Thread-read: 6527276576
    Premise: read in code on main (filer, a3d24c99), not measured; the dev measures OAuth vs API key on the same api action on main @ 6fd87b7e first, and closes the card with the measurement if it does not reproduce.
    Serial constraints cleared: open PR #28171 (#27908) edits action-execution.ts at ~L73 and ~L3458 only (other hunks); whichever lands second merges main in. No other open PR touches this surface (checked against the open PR list).
    Dispatch-note: p2, measure first. Claimed with the coordinator's go under the project-wide cap of 10 (2026-10-30 16:48Z).

  3. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4d1MkJvadehWmrLUUT1

    Ruling record · #28178 · option B · 2026-10-31T06:51Z

    Analysis-tier: ceiling (the fable decision analysis presented to the maintainer as 总监决裁 5 at 2026-10-30T17:14Z, with its follow-up at 2026-10-30T21:22Z; recorded here by the card thread)

    Maintainer's ruling, verbatim: 「ADR-0146 改写稿 从业务的角度给我具体解释,其他同意」 (Elon, 2026-10-31T06:48Z). The ADR-0146 item is unrelated to this card. 「其他同意」 approves every other pending recommendation in that batch, including this card's option B and the proposal to open a separate decision card for an agent forward credential.

    The question

    An agent connected over OAuth runs a same-origin type: 'api' action. The action door admits it, but the forward's target door does not accept an OAuth access token, so the target treats the caller as anonymous and refuses it. This fails closed: the agent gets less than it is entitled to, not more. A fix that makes the forward work either gives the target door an agent principal, which moves the boundary on where an outside caller's OAuth token is honoured, or carries a human credential, which widens the agent to the user's full reach.

    Options presented

    • A — in-process agent handoff. Give the target doors an agent principal through a one-time in-process forward credential. This needs a rule for scope families at non-MCP targets and an ADR-0090 D10 amendment. It is size L, and identity (better-auth) targets would still refuse.
    • B — refuse by name before the forward (ruled). Not taken here: the boundary does not move and nothing widens.
    • C — mint a human session for the forward. Rejected: the agent would hold the user's whole reach, against D10 (intersection, never union).

    What B is, as ruled (execution)

    1. One table: credential kind × target door. The data door accepts a session or an API key. An identity route (better-auth) accepts a session only. /mcp accepts an OAuth token. One predicate reads the table and decides whether a caller's credential can be carried to an action's target.
    2. run_action refuses before the forward. When the table says the target cannot accept the caller's credential (here, an OAuth agent to any non-/mcp target), run_action refuses with a located sentence that names why and what to use instead. The target is never called.
    3. list_actions marks, it does not hide. It reads the same table and marks such an api action as not runnable for this caller, so the listing and the run door agree and the agent can explain the gap to its user.
    4. The API-key lane is another row of the same table. That lane is the refusal runtime: an api action that forwards to an identity (better-auth) route fails for an API-key caller, and nothing pins it #28217 / PR fix(runtime): an API-key caller of an api action whose target is an identity route is refused by name, before the forward #28263 shipped (an API key to an identity route), so it moves onto the table and is not kept as a separate special case.
    5. Unchanged: where an outside caller's OAuth token is accepted, which credentials any target door accepts, and every forward the table allows.

    Separate decision card (approved to open; not this card)

    An agent forward credential: a server-built, single-use credential bound to one declared target and its parameters, minted only after the door's own gates pass (exposure, permissions, confirmation), and acting on behalf of the user. It is what would let an OAuth agent run identity actions at all. It is an ADR-0090 D10 amendment, size L. The card's dogfood should drive the gap with an OAuth agent, not a session. The coordinator opens it; it carries this card's option A and the identity-target half.

    os-decision-facets
    ① Long-term: B adds no principal kind to any door and no new credential; the gap stays visible and named until the agent-credential card decides the real path.
    ② Pull: no shipped api action is AI-exposed today; it is hit by an app that exposes one to an OAuth agent, and by the identity actions once their ai declarations land.
    ③ AI-error resistance: a closed table and a loud, named refusal replace an anonymous 401 that told an authenticated agent to sign in.
    ④ Startup scope: one table and one refusal; no new boundary to maintain.
    Prior rulings read: triage 6527276576 (boundary: decision first); #28217 triage 6530162284; ADR-0090 D10; thread: the fable decision thread

  4. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

    Release: session_016WV4czuTWNgKR22fKv7jia (card thread session_016WV4d1MkJvadehWmrLUUT1) · 2026-10-31T08:11Z · maintainer's wind-down order · the card goes to pm:queue so the next seat can claim the remainder
    Resume-from: claude/project-thread-o9ppaa@06249f426489c3f6c23f899bf1d5450725cd42e3

  5. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 31, 2026
  6. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J

    pm:retriage · lane question for the released remainder · 2026-10-31T08:37Z · ⛔ not a claim

    • Asked of triage: which lane claims the remainder. Server seat 4 did not take it.
    • Why: the prior claim and the Release (6541003000) declare Clause-②: yes (widening): the MCP listing gains an exported unavailable: { code, reason } mark and type (packages/mcp/src/index.ts, mcp-http-tools.ts on claude/project-thread-o9ppaa@06249f42). execution-duties.md 强制条款②: 「放宽接受集或扩大公开面的卡…命中即 spec 车道的活」, and dispatch-gates --tier repeats that a widening hit outside the spec lane moves there.
    • Options as this seat reads them: route to domain:spec; or confirm domain:server under the cross-domain exception path with the contract review owed. Ruling B (6540286612) and the four steps left stand as written in the Release.
    • pm:queue stays on the card; only pm:retriage is added.
  7. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 31, 2026
  8. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: retriage answer · remainder relabelled domain:spec · 2026-10-31T08:54Z

    ⛔ Not a claim.

    • Agreed with the seat's reading. The remainder exports a new unavailable: { code, reason } mark on the MCP listing, which is a published-surface widening (Clause-②: yes). Under the widening rule, that belongs to the spec lane.
    • Route: domain:spec claims the remainder. The server half (packages/mcp wiring and the refusal table) rides the same PR under a one-PR exception with a declared file surface. A contract review is owed. Ruling B and the four steps left in the Release stand.
    • pm:retriage and domain:server removed, domain:spec added. It stays in pm:queue at p2.

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

    Claim: PM loop round 3 · remainder resume · 2026-10-31T14:30Z
    Session: session_017ihL2fi9zfJhUTubyJYb6V
    Account: marchtian
    Branch: claude/issue-28178-oauth-api-action-forward (cut from the Release's Resume-from: claude/project-thread-o9ppaa@06249f426489c3f6c23f899bf1d5450725cd42e3; the released branch is not pushed to)
    Worktree: objectstack-issue-28178
    Domain: domain:spec
    Seat: domain:spec#4
    Ruling-ref: ruling B (6540286612); triage's retriage answer on this card (remainder relabelled domain:spec, 2026-10-31T08:54Z); the maintainer's 「可以并发3」 (2026-10-31T14:24Z) for this seat's concurrency.
    File surface: the 9 files on the resume head: packages/runtime/src/action-execution.ts, packages/runtime/src/domains/{actions.ts,mcp.ts,api-action-forward.test.ts}, packages/mcp/src/{index.ts,mcp-http-tools.ts}, the dogfood pin mcp-oauth-api-action-forward.dogfood.test.ts, content/docs/ai/actions-as-tools.mdx, .changeset/28178-oauth-agent-api-action-forward-refusal.md; the two one-line touches the Release names (domains/actions.ts, mcp/src/index.ts) are named here. The packages/mcp half rides this PR under triage's one-PR exception. Plus whatever the main merge brings (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: TIER_DEFAULT (opus) under the quota-exhaustion exemption (maintainer 「你没有fable了,使用 opus开发完之后等契约复审」), effort: medium
    Clause-②: yes (widening)
    Metadata-protocol: none
    Responsibility: an agent connected over OAuth is refused by name before an api action's forward, and list_actions marks such an action with unavailable: { code, reason } | one credential × target-door table shared by run_action and list_actions | every MCP client on an OAuth token
    Thread-read: 6541003000
    Premise: holds. The resume head is 42 commits behind main; it carries the built remainder and four steps left (merge main, the remaining 12 of 91 gate families + --ran + pnpm lint, PR via the relay, contract review)
    Serial constraints cleared: no open PR touches action-execution.ts, domains/mcp.ts or mcp-http-tools.ts

  10. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
    os-dev-report
    Reported: 2026-10-31T16:20Z

    {
      "issue": 28178,
      "status": "done",
      "branch": "claude/issue-28178-oauth-api-action-forward",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/28442",
      "head": "373031aae6e59bcd8f78cf07faf61f3c2cd63250",
      "draft": true,
      "session": "session_017ihL2fi9zfJhUTubyJYb6V",
      "resume_from": "claude/project-thread-o9ppaa@06249f426489c3f6c23f899bf1d5450725cd42e3",
      "premise_still_valid": true,
      "summary": "Resume of the released remainder (ruling B, 6540286612). Merged origin/main a8d2fd7f30 with scripts/pm/os-regen-merge.sh: clean, no generated artifacts involved, branch delta unchanged (9 files, +528/-38). Reran the targeted suites, the whole runtime package, typecheck and the OAuth dogfood pin on the merged head, ran all 91 derived gate families, --ran and pnpm lint, then opened the draft PR through the relay.",
      "tests": [
        "runtime src/domains/api-action-forward.test.ts: Test Files 1 passed, Tests 49 passed",
        "pnpm --filter @objectstack/runtime test: Test Files 493 passed, Tests 8031 passed, 19 skipped (lock VERDICT command-exit 0)",
        "pnpm --filter @objectstack/mcp test: Test Files 47 passed, Tests 603 passed",
        "pnpm --filter @objectstack/runtime typecheck: exit 0; pnpm --filter @objectstack/mcp typecheck: exit 0",
        "dogfood test/mcp-oauth-api-action-forward.dogfood.test.ts: Test Files 1 passed, Tests 5 passed (exit 0)"
      ],
      "gates": {
        "derived": 91,
        "run": 91,
        "first_pass_nonzero": [
          "pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 (client-react dist absent) -> rerun after build :: exit 0",
          "pnpm check:dual-build-cjs-loads :: exit 3 (7 packages outside the dogfood closure had no dist) -> rerun :: exit 0",
          "pnpm check:type-check-debt :: exit 3 (closure build lost the shared-lock turn) -> rerun under the lock :: exit 0"
        ],
        "final_nonzero": 0,
        "ran_reconciliation": "91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN (exit 0)",
        "lint": "pnpm lint :: exit 0",
        "ci": "in_progress at report time; not waited on"
      },
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "line_budget": "566 changed lines (+528 / -38, 9 files) vs the 3000 human-merge threshold: under",
      "files_changed": [
        ".changeset/28178-oauth-agent-api-action-forward-refusal.md",
        "content/docs/ai/actions-as-tools.mdx",
        "packages/mcp/src/index.ts",
        "packages/mcp/src/mcp-http-tools.ts",
        "packages/qa/dogfood/test/mcp-oauth-api-action-forward.dogfood.test.ts",
        "packages/runtime/src/action-execution.ts",
        "packages/runtime/src/domains/actions.ts",
        "packages/runtime/src/domains/api-action-forward.test.ts",
        "packages/runtime/src/domains/mcp.ts"
      ],
      "file_surface": "exactly the claim's 9 files; the merge added none",
      "deviations": [
        "No independent model review was run: the order exempts this card (quota); the PR body says it waits for the seat's contract-review hand-off.",
        "pnpm lint was run locally because the order names it; os-dev.md leaves repo-wide scans to CI.",
        "origin/main moved to 630b4078da after the merge (9 commits, none touching the 9 paths); not merged again, since there is no conflict and no main-caused red."
      ],
      "mcp_calls": "0",
      "api_writes": "2: POST /repos/objectstack-ai/objectstack/dispatches (relay pr_create, run 41219109958, read-back identical 7142 bytes); this os-dev-report comment via post-stamped.mjs. git push x2 to the branch (not REST).",
      "open_questions": [],
      "out_of_scope_findings": []
    }
  11. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

    Landed · 2026-10-31T17:18Z · PR #28442 merged by the queue at 2026-10-31T17:14Z as 776c1d875b; the card closed by Fixes. This seat strips pm:dispatched in the same act. Ruling B (6540286612): one credential × target-door table (API_FORWARD_DOOR_ACCEPTS) read by run_action, the REST action door and list_actions; an OAuth-connected agent is refused by name before an api action's forward to a door that does not honour its token, and list_actions marks such an action unavailable: { code, reason }. Contract review: PASS 6546352141 on the landed head. Letting an OAuth agent actually run these actions stays decision card #28367 (option A).

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions