Repository navigation
fix(runtime): list_actions marks revoke_api_key unavailable to an API-key caller, with the API-key guard's own refusal (#28943) - #29036
Conversation
…n API-key caller, with the guard's own refusal The forward credential marker now asks the API-key management guard about the data-door request a forward makes, so revoke_api_key is listed with the guard's PERMISSION_DENIED and sentence, and the run doors answer that refusal before forwarding. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oTRCUNQ4EA3YV84sZ
📓 Docs Drift CheckThis PR changes 1 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a1f31b6f04d1d718b2085d9c6daaa7b4e676707f && git checkout a1f31b6f04d1d718b2085d9c6daaa7b4e676707f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin de68479670e751ba13c6f1a10d4d85a772a478b7 98b8895f05510e7247801a5439fecd6f2950c9fa && git checkout -B drift-repro de68479670e751ba13c6f1a10d4d85a772a478b7 && git merge --no-ff 98b8895f05510e7247801a5439fecd6f2950c9fa
node scripts/docs-audit/affected-docs.mjs --json de68479670e751ba13c6f1a10d4d85a772a478b7
|
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #28943. 4 files (+242 / −9): apiForwardCredentialRefusal plus two private helpers in runtime/src/action-execution.ts, a comment in domains/mcp.ts, unit pins and a @objectstack/runtime patch changeset. Metadata-protocol: none. No new export, no packages/spec/**, no gate, no new error code. |
| Rule | One source: when the target's door accepts the caller's credential, the marker now also asks @objectstack/core's apiKeyManagementCredentialRefusal (already exported, already imported by this file, so no cycle) about the data-door request the forward makes, and returns that guard's own 403, PERMISSION_DENIED and sentence. list_actions marks revoke_api_key with it for an osk_ caller, and both run doors answer it before forwarding instead of after. The guard is unchanged, and only api_key is ever refused here: sessions, reads of sys_api_key and every other object are unchanged. |
| Premise | On main the guard refuses every non-read data-door op on sys_api_key under an API key; the actions landing there are revoke_api_key and restore_api_key (and the separate /keys mint). list_actions offered only revoke_api_key to an osk_ caller, the one ai.exposed. None in examples/ or hotcrm. |
| Evidence | 3 pins red on base and green on head; the 2 controls (session revoke runs; a crm_case write under a key runs) green on both. Ablation red, restore clean. Full runtime suite (8272), typecheck in full, eslint, 61/61 derived gates, 0 NOT-MEASURED. CI on head is running; auto-merge waits for it. PR #29005 (#28932) edits another hunk of the same file and is ahead in the queue. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, with every check green or an expected skip. |
Generated by Claude Code
…-jqgfyc # Conflicts: # packages/runtime/src/domains/api-action-forward.test.ts
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY 席位核对 · ACCEPT r2 (new head) ·
|
| item | reading |
|---|---|
| Shape | Unchanged from 6570583249: 4 files, runtime/src/action-execution.ts, domains/mcp.ts (comment), domains/api-action-forward.test.ts, a @objectstack/runtime patch changeset. Not governed. |
| Merge | git show --remerge-diff 98b8895f has one conflict, in the test harness boot of api-action-forward.test.ts: main added PING_LOCAL to objectDef.actions; this branch added the served map over extraObjects. The resolution keeps both (main's action list, this branch's map). No other hunk differs from a clean merge; the rule in action-execution.ts is the ACCEPTed one. |
| Evidence | CI on 98b8895f: 35 success, 13 skipped, 0 failed; check-expected-skips.mjs --pr 29036: OK, every skip in main's roster. The card thread reported 78/78 in the file and runtime pnpm typecheck exit 0 on this head (amendment 6571831222). mergeable_state clean. |
| Out of scope | none new. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): auto-merge through the relay, every check green or an expected skip. |
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Fixes #28943
Clause-②: no
Metadata-protocol: none
维护者速读
list_actions时,revoke_api_key不再显示为可运行。它带上unavailable,内容是 API key 管理守卫自己的PERMISSION_DENIED和原句,与run_action的拒绝一致。sys_api_key的 Revoke 被列为可运行;随后该对象的守卫apiKeyManagementCredentialRefusal以 403 拒绝写入。这是 18.3.0 RC 跑测 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 中唯一的 FAIL。sys_api_key、其他对象,以及 session 调用方的列表和运行都不变。REST 动作入口和 MCPrun_action对同一请求改为在转发前给出同样的 403 和原句,不再转发后才被拒。回滚即还原本 PR。packages/runtime/src/action-execution.ts的apiForwardDataDoorGuardRefusal和apiForwardDataDoorOperation。pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-forward.test.ts -t 28943,预期 5 个测试全绿。Measure (step 1, on
mainf7c545a)sys_api_key: create, update (PATCHandPUT), delete,bulk(the batch routes),import,clone, and any write verb a door adds later. The guard is asked at every per-object gate: the runtime dispatcher'srefuseUnexposedDataAction,RestServer.enforceApiAccess(and per op on its cross-object batch), and the stdio MCP write arms. The/api/v1/keysmint is a separate session-only refusal indomains/keys.ts, with its own sentence.apitarget lands on one:sys_api_key.revoke_api_keyandsys_api_key.restore_api_key(bothPATCH /api/v1/data/sys_api_key/{id}), pluscreate_api_keyon the/keysmint. No action inexamples/or hotcrm targets either (the oneexamples/app-todohit is an e2e that runs an undeclaredrotateand expects the refusal).osk_caller onmain:revoke_api_keyonly. It is the one withai.exposed;restore_api_keyandcreate_api_keydeclare noaiblock, soactionAiExposureErrorleaves them out oflist_actions.What changes
One source. The guard's predicate,
apiKeyManagementCredentialRefusal, is already exported from@objectstack/core, andaction-execution.tsalready imports it for its per-object gate. So there is no dependency cycle, and no new export on any package root. The marker now calls it directly.apiForwardDataDoorOperation(method, path)(new, module-private) reads the data-door request a forward makes: the object segment of/api/v1[/environments/:id]/data/:object[/…], and the operation in the doors' own names.GET/HEAD,POST /:object/queryandPOST /:object/:id/pageare reads;PATCH/PUTisupdate,DELETEisdelete,POSTto the collection iscreate, and any otherPOSTis named by its last segment. Only the read-or-write reading is load-bearing, because the guard refuses every operation it does not list as a read.apiForwardDataDoorGuardRefusal(kind, action, target)(new, module-private) asks the guard about that request when the caller's credential isapi_key, and returns the guard's ownstatus(403),code(PERMISSION_DENIED) andmessage. Nothing is restated.apiForwardCredentialRefusal: where the target's door accepts the credential, it now returns that answer instead ofnull. The door table, the identity and OAuth refusals, and the agent forward credential are untouched.Both readers of
apiForwardCredentialRefusalpick this up with no change of their own:list_actionsmark (domains/mcp.ts, comment only), which carries the guard's code and sentence asunavailable;forwardApiAction, behind the REST action door and MCPrun_action, which now answers that refusal before forwarding. Before, it forwarded and relayed the data door's 403 with the same code and sentence.The listing judges the declared target (
{id}unfilled) and the run door judges the filled path. Both readaction.method, and the object segment is never a token, so they judge the same request.⛔ The guard's refusal is unchanged, and there is no new error code, no
packages/spec/**change and no new gate. Reads ofsys_api_key, every other object, and the session caller's listing and run are unchanged.Changeset: patch on
@objectstack/runtime(Metadata-protocol: nonegives patch).Tests
New describe
[#28943]inpackages/runtime/src/domains/api-action-forward.test.ts. The harnessbootgainsextraObjects, which serves asys_api_keycarryingrevoke_api_keyexactly as declared, and acrm_casecarrying the samePATCHshape as the control.list_actionsunder an API key:revoke_api_keycarriesunavailable= the guard's code and sentencerun_actionand the REST action door under an API key: 403,PERMISSION_DENIED, the listed sentence; nothing forwardedsys_api_keywrite is refused, its reads are not, a neighbouring object name is not; session and no-credential callers are refused nowhererevoke_api_keyis listed with no mark, andrun_actionforwardsPATCH /api/v1/data/sys_api_key/key_1withrevoked: truecrm_caseunder an API key is listed with no mark and forwarded, the key carried acrossThe two controls are green on base by design: they pin what must not change.
Ablation:
scripts/ablation-replace.mjsreplaced the marker's read of the guard withreturn null. The mutation landed (anchor 1 to 0), and the three pins went red (3 failed, 2 passed). The restore was clean: the blob equals HEAD andgit diff HEADis empty.Suite, typecheck, lint, gates
On head
c3b472f4, after a whole-repopnpm buildunder the verify lock (exit 0):@objectstack/runtimesuite under the lock: 8272 passed, 19 skipped, exit 0.@objectstack/coreis not touched;pnpm typecheckin full for runtime: exit 0, includingcheck:test-typecheck(ledger unchanged);eslint --no-inline-config srcfor runtime: exit 0;dispatch-gates.mjs --commandsderived 61 commands (includingcheck:route-ledger-census), each run with its exit captured before any pipe, all exit 0.--ran: 61 derived, 61 run, 0 NOT-MEASURED.A live MCP probe on a showcase boot was not run; the in-process doors above are the evidence.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oTRCUNQ4EA3YV84sZ