Skip to content

MCP list_actions offers sys_api_key.revoke_api_key as runnable to an API-key caller, and run_action then refuses it 403 #28943

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).

立卡门类别: ① 产品缺陷 — 具名落点 (apiForwardCredentialRefusal / API_FORWARD_DOOR_ACCEPTS in packages/runtime/src/action-execution.ts) + 复现
reach: 公开入口实测一次错误结果 — MCP /api/v1/mcp list_actions under an osk_ key lists revoke_api_key with no unavailable, and run_action then answers 403 (three fresh boots at 7fa9873)
业务理由: an operator who runs an agent headless on an API key is told the key-revoke action is runnable, is asked by the agent to confirm it (428), and then the action fails. That wastes a human confirmation and breaks the agent's flow at the moment someone is trying to revoke a key. Fixing the marker keeps the list/run promise #28178 introduced. Not doing it: every API-key agent keeps offering an action it can never complete.
动手的读者: the domain:server seat (runtime action execution, owner of the #28178 marker); triage first touch confirms the lane, then os-dev.
查重: revoke_api_key → 16 hits (0 open cards; 2 open are QA run records #27392 / #27438), list_actions unavailable → 13 hits (0 open cards; #28178 closed is the related fix), apiForwardCredentialRefusal → 0. No duplicate.

QA-source: #28942 · ai.mcp-run-action-exposure-gate · negative[1]

What happens. list_actions marks an api action unavailable when its forward target cannot accept the caller's credential (#28178; apiForwardCredentialRefusal in packages/runtime/src/action-execution.ts). The marker reads a per-door table (apiForwardTargetDoor / API_FORWARD_DOOR_ACCEPTS) that classes revoke_api_key's target, PATCH /api/v1/data/sys_api_key/{id}, as the data door, and the data door as accepting api_key. But the API-key management guard (packages/core/src/security/api-key-management-credential.ts) refuses any API-key-authenticated change to sys_api_key with 403 PERMISSION_DENIED.

So an MCP agent connected with an osk_ key is told revoke_api_key is runnable (no unavailable), confirms it with the human (428 first), and is then refused, with nothing changed. That is the list/run disagreement the checklist negative forbids. The ADR-0011 identity-actions addendum already says this target accepts a signed-in session, so the table is the outlier. The refusal itself holds; this is a listing-honesty defect, not an authorization hole.

Reproduction (fresh stock showcase boot at 7fa9873; reproduced on three fresh boots):

  1. Admin, signed in: POST /api/v1/keys {"name":"r2-repro"} → key K; POST /api/v1/keys {"name":"throwaway"} → id T.
  2. MCP Streamable HTTP on /api/v1/mcp with Authorization: Bearer K (initialize, then notifications/initialized).
  3. tools/call list_actions {}: the entry revoke_api_key (objectName sys_api_key, requiresConfirmation true) has no unavailable key, while every identity-route action in the same list carries unavailable: {code: UNAUTHENTICATED, reason: "…accepts a signed-in session only, and this request was authenticated with an API key…"}.
  4. tools/call run_action {"actionName":"revoke_api_key","objectName":"sys_api_key","recordId":T,"confirm":true} → isError {"code":"PERMISSION_DENIED","status":403,"message":"PERMISSION_DENIED: Changing an API key requires a signed-in session. … a request authenticated with an API key cannot revoke, restore or change API keys."}.
  5. Admin GET /api/v1/data/sys_api_key: T reads revoked: false.
  • Expected: the listing marks revoke_api_key unavailable for an API-key caller (with the guard's own sentence), or the run succeeds.
  • Actual: listed as runnable, then refused 403.
  • Control: the same run_action over MCP with the admin's session bearer → ok: true, T reads revoked: true.

Fix direction: teach the credential table that the sys_api_key write routes refuse api_key, or derive the marker from the same guard. Related: #28178 (closed).

Full evidence: QA run record #28942.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: ai.mcp-run-action-exposure-gate

    Triage: priority:p1 · domain:server · pm:queue · 2026-11-02T05:04Z

    ⛔ Not a claim.

    • Read fresh. apiForwardCredentialRefusal / API_FORWARD_DOOR_ACCEPTS (packages/runtime/src/action-execution.ts) class the sys_api_key write route as accepting api_key, so list_actions offers revoke_api_key to an API-key caller that run_action then refuses. The card states the refusal holds and this is not an authorization hole, so there is no SECURITY flag. It is graded p1 under 「发版本优先的都p1」 because negative[1] is the only FAIL in the 18.3.0 release-candidate run QA run · release-18.3.0-w4-c3a (44/44) · 7fa9873f · 2026-11-02 · 41 PASS / 2 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #28942, on a P1 item at the release head. The location and fix direction are named, so it goes to pm:queue in domain:server.

    Generated by Claude Code

  2. added
    area:aiAI-native — agent / tool / skill metadata, and the MCP surface an agent drives
    domain:serverDomain lane — seat card indexed by label:pm:seat
    priority:p1High: required for production / M2
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

    Claim: PM loop round 1 · card thread · 2026-11-02T05:19Z
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01FEkQ5oTRCUNQ4EA3YV84sZ (the #28412 / #28717 / #28771 / #28809 / #28858 / #28874 card thread, continuing; restarted from main after PR #28925)
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-jqgfyc (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: triage#1 graded the card p1 (05:04Z, the only FAIL of the 18.3.0 RC run #28942) and ruled it a listing-honesty defect, not an authorization hole. Route (the dispatching seat's call): the list_actions marker derives from the API-key management guard's own predicate, one source, with the guard's sentence. Dispatched in the seat-1 split of the server queue under 「队列进了很多任务,抓紧派发」 (Leehom, 2026-11-02T05:15Z) and 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z).
    File surface: apiForwardCredentialRefusal / apiForwardTargetDoor / API_FORWARD_DOOR_ACCEPTS (packages/runtime/src/action-execution.ts) and, if the predicate must be shared, an export beside packages/core/src/security/api-key-management-credential.ts; their tests and changesets. ⛔ No packages/spec/**, no change to the guard's refusal, no new error code.
    Container & model: S, mode:thread, model and effort as the thread was opened
    Clause-②: no (expected: one entry of an existing answer gains its existing unavailable member for an API-key caller; the dev measures)
    Metadata-protocol: per AGENTS.md, measured by the dev (likely none)
    Responsibility: the code is the action credential marker of @objectstack/runtime and the API-key management guard of @objectstack/core · the platform path is an API-key MCP caller offered revoke_api_key as runnable, then refused 403 · reached by every list_actions under an osk_ key
    Thread-read: none
    Premise: holds on main 7fa9873f (the card's three fresh boots)
    Serial constraints cleared: #28178 closed. No open server card edits action-execution.ts's credential table; #28932 (seat 1, another thread) edits buildApiActionRequest in the same file, so merge origin/main once if it lands first.
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
    os-dev-report
    {
    "issue": 28943,
    "status": "done",
    "branch": "claude/project-thread-jqgfyc",
    "pr": "#29036",
    "session": "session_01FEkQ5oTRCUNQ4EA3YV84sZ",
    "premise_still_valid": true,
    "summary": "Measure on main f7c545a: the guard (core apiKeyManagementCredentialRefusal) refuses every non-read data-door operation on sys_api_key to an api_key admission (create, update PATCH/PUT, delete, bulk, import, clone, any later write verb), asked at refuseUnexposedDataAction, RestServer.enforceApiAccess and the stdio MCP write arms; the /api/v1/keys mint is a separate session-only refusal with its own sentence. Actions landing on one: sys_api_key revoke_api_key and restore_api_key (PATCH /api/v1/data/sys_api_key/{id}) and create_api_key (the /keys mint); none in examples or hotcrm. list_actions offered only revoke_api_key as runnable to an osk_ caller (the only ai.exposed one). Fix, one source: the guard predicate is already exported from @objectstack/core and already imported by runtime action-execution.ts, so no cycle and no new export. apiForwardCredentialRefusal, where the target door accepts the credential, now asks the guard about the data-door request the forward makes (module-private apiForwardDataDoorOperation reads object and operation from method and path; apiForwardDataDoorGuardRefusal returns the guard's own 403, PERMISSION_DENIED and sentence). list_actions marks revoke_api_key unavailable with that code and sentence; forwardApiAction (REST action door, MCP run_action) answers the same refusal before forwarding instead of relaying the data door's after. Guard unchanged, no new code, no spec, no new gate; sys_api_key reads, other objects, and the session caller's listing and run unchanged. Patch changeset on @objectstack/runtime.",
    "tests": "New describe [#28943] in packages/runtime/src/domains/api-action-forward.test.ts (boot gains extraObjects: sys_api_key with revoke_api_key as declared, crm_case with the same PATCH shape). Red on base f7c545a, green on head: list_actions under a key marks revoke_api_key with the guard code and sentence; MCP run_action and REST action door under a key answer 403 PERMISSION_DENIED with the listed sentence and forward nothing; request-by-request reading (writes refused, reads and a neighbouring object not, session and no-credential callers never). Controls green on both by design: session listing unmarked and run_action forwards the PATCH with revoked true; crm_case write under a key unmarked and forwarded. Ablation via ablation-replace.mjs (marker read of the guard to return null): landed, 3 pins red, restore blob equals HEAD and git diff HEAD empty.",
    "gates": "Head c3b472f after a whole-repo pnpm build under the lock exit 0: full @objectstack/runtime suite under the lock 8272 passed 19 skipped exit 0 (core not touched); pnpm typecheck in full for runtime exit 0 incl check:test-typecheck (ledger unchanged); eslint --no-inline-config src exit 0; dispatch-gates --commands derived 61 (incl check:route-ledger-census), each run with its exit captured before any pipe, all exit 0; --ran: 61 derived, 61 run, 0 NOT-MEASURED. origin/main moved 12 commits, none touching the three runtime files (#28932 not landed); git merge-tree clean. No live MCP probe. CI on PR #29036 not awaited.",
    "dogfood_battery": "n/a, no ! in the PR title and no (narrowing) arm (Clause-②: no)",
    "mcp_calls": "3 — mcp__github__issue_read x3 (get #28943, get_comments #28943 twice, the second to re-read the newest Claim before editing); no write tool",
    "api_writes": "3 — plain git push of claude/project-thread-jqgfyc (new branch, c3b472f); relay fleet-write pr_create #29036 (read-back identical, assignees none, no labels named); this os-dev-report via post-stamped.mjs --comment=28943",
    "open_questions": [],
    "out_of_scope_findings": []
    }

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

    Release: server seat 1 shift close · 2026-11-02T07:37Z
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat that claimed this card); card thread session_01FEkQ5oTRCUNQ4EA3YV84sZ (paused)
    Who / what / where: Leehom (os-project-manager) in the server-lane project chat — 「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」 (2026-11-02T07:11Z), 「自动合并中的也交给下一任跟进」 (2026-11-02T07:23Z), 「我说的是pm-dispatch 中车道下班的流程,都要走完」 (2026-11-02T07:33Z). Shift close returns every claim to the queue (seat-post-protocol.md 交接收尾清单 step 2); no stay-behind.
    Resume-from: claude/project-thread-jqgfyc@c3b472f49e897797ecaef905d7b107bec3e873c6 — PR #29036, ACCEPTed by the seat on this head. Dequeued on a MERGE_CONFLICT with #29005; auto-merge is OFF. Next: merge origin/main onto the branch, push, re-run the seat check on the new head, re-arm.
    Successor: read the handoff 6571543819 on seat post #6367 first; land PR #29036 on green, write the landing record, close this card. ⛔ Do not re-dispatch the card from scratch.

    State: pm:dispatched → pm:queue, assignee os-project-manager cleared, recorded by this release.


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

    Resume-from amendment · 2026-11-02T07:44Z: the card thread merged origin/main into the branch after the release was written (plain push, no rebase). Resume-from is now claude/project-thread-jqgfyc@98b8895f05, PR #29036. Conflict was the harness objectDef line in api-action-forward.test.ts, both sides kept; the thread reports 78/78 green and runtime pnpm typecheck exit 0. The seat's ACCEPT was on the pre-merge head: the successor re-runs the seat check on 98b8895f05, then arms auto-merge.


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Claim: PM loop round 1 · takeover of a released card · 2026-11-02T08:15Z
    Session: session_01ADHkmQjPiXgNoSaEpqeGvY
    Account: os-steve (the card's assignee, set in the same round)
    Branch: claude/project-thread-jqgfyc
    Worktree: the predecessor card thread's (paused); no new dev is dispatched
    Domain: domain:server
    Seat: domain:server#1
    File surface: unchanged from the predecessor's claim 6569706666 (apiForwardCredentialRefusal and its readers in packages/runtime/src/action-execution.ts, the forward test); the open PR #29036 is the surface.
    Container & model: S, mode:thread, landing only (no new build)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: as in the predecessor's claim (unchanged)
    Thread-read: 6571831222
    Premise: holds (PR #29036 ACCEPTed by the predecessor seat)
    Serial constraints cleared: none new; the seven PRs this seat inherited are its only in-flight surface.
    Resume-from: claude/project-thread-jqgfyc@98b8895f05510e7247801a5439fecd6f2950c9fa

    Takeover record: the predecessor (os-project-manager, session_01R1q2xdtYgU6FKrZ7LgjPCC) released this card in 6571709827 at its shift close (Leehom: 「自动合并中的也交给下一任跟进」). This seat takes it on the maintainer's summons in the Claude Code project "objectstack spec" (「server 1 号座位你也接手开席位」), seat 2 (#20966) having left it to seat 1. Last pushed head: 98b8895f05 (PR #29036). Dequeued on a MERGE_CONFLICT; the branch merged origin/main at this head after the predecessor's ACCEPT. This seat re-runs the seat check on this head, then re-arms.

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Landed · 2026-11-02T10:40Z

    Probe Reading
    PR #29036 MERGED 2026-11-02T09:37:10Z, merge commit af8448fe9e, through the merge queue
    #28943 closed completed by Fixes #28943

    pm:dispatched is removed. Seat record: seat ACCEPT r2 6572410751 on head 98b8895f, takeover claim 6572348542.

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

Metadata

Metadata

Assignees

Labels

area:aiAI-native — agent / tool / skill metadata, and the MCP surface an agent drivesdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions