Skip to content

Commit 242665f

Browse files
docs(qa): re-point ai.mcp-run-action-exposure-gate to the per-action sys_* gate (#29931)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi9paTx1Lu8ENYwwssthfK3h)_ Fixes #29892 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:测试清单里"MCP 动作暴露闸"这一项(ai.mcp-run-action-exposure-gate)升到第 7 版,第 4 条验收和第 6 步改成按动作判定:系统对象上的动作自己声明了 ai.exposed 才会被 AI 列出并执行,没声明的照旧拒绝。 - 为什么改:旧条款还写着"系统对象一律拒绝",而 #27681、#27900 的裁决早已改为逐动作开放,所以这条 P1 发版证据每轮测试都报 FAIL,把它真正找到的缺陷 #28943 埋在一行常红里。 - 风险与回滚:只改一份清单 JSON,不碰产品代码,回滚就是 revert 本 PR;条款强度不降,仍要求看到一个未声明的系统动作被拒且零副作用,同时看到一个已声明的动作被放行。 - 先看:docs/qa/platform-checklist/areas/ai.json 该项的 acceptance[4](拒绝半与放行半),其次 steps[6] 和新增的 fixtures.requires[4]。 - 两分钟验证:运行 `node scripts/check-platform-checklist.mjs`,预期打印 `check-platform-checklist: OK` 并以 0 退出。 - 需要你做的:无,席位落地。 ## What changes One item revision in `docs/qa/platform-checklist/areas/ai.json`: `ai.mcp-run-action-exposure-gate` goes from revision 6 to 7, with a `history` row citing #29892 and the three rulings (6513869839 on #27681, letter B; 6523087116 and 6540309323 on #27900, the ADR-0011 identity-actions addendum). - **acceptance[4]** no longer says system objects are refused wholesale. It now says a system-object action passes the MCP system-object guard only on its own `ai.exposed`, and it names both halves on `sys_api_key`: - refused: `create_api_key` (no `ai` block; a credential-minting action, held human-only by ruling 6513869839 parameter 2) is absent from `list_actions`, is refused in both live spellings (the tool layer's `systemObjectActionRefusal` with `objectName`, the runtime's `systemObjectActionExposureError` without it), and no key is created; - exposed: `revoke_api_key` (`ai: { exposed: true, requiresConfirmation: true }`, bucket C) is listed with `aiExposed: true` and, called with the admin's session token and `confirm: true`, answers `ok: true` and the probe key reads `revoked: true`. The verify names both FAIL shapes: a `sys_*` action with no `ai` block that is listed or runs, and an exposed one refused with either system-object text. - **steps[6]** names those two actions. "any sys_* action name" can now pick an exposed one, and then the step proves nothing. - **fixtures.requires[4]** (new, appended, so no index shifts) stages the probe key and the session bearer. Under an `osk_` key, `revoke_api_key` is listed with `unavailable` and refused by its target's credential gate, so the exposed run needs a signed-in session. - **source** gains three symbol anchors: `systemObjectActionExposureError`, `isListableAction` and `sys-api-key.object.ts#revoke_api_key`. All of them resolve (1134/1144 to 1137/1147 anchors resolved). Riders from the same rulings, which are deviations from the card's two named fields: - the **title** said "refuses system objects"; it now says "admits a system-object action only on its own ai.exposed"; - **acceptance[1].verify** called `showcase_portfolio_snapshot` "the one exposed action". The listing now also carries the platform's ai-exposed `sys_*` identity actions, and an entry marked `unavailable` is listed by contract. The clause itself is unchanged. The statement that no exposed action gates a permission still holds, because no `sys_*` action declares `requiredPermissions`. Left intact on purpose: **negative[1]**, the list/run agreement that found #28943. ## Tests Docs only, with no product code. The ruled behaviour was read on `main` at `a04e9d76c1`, and the two unit pins that drive it were run on this branch's tree, whose product code is identical to that base: - `packages/runtime/src/action-execution.ts` L141-158 `systemObjectActionExposureError`: `return null` only on `action?.ai?.exposed === true` (L154), with the refusal text at L156. Its call site in `invokeBusinessAction` is L4260-4261. The runtime listing asks the same predicate at `packages/runtime/src/domains/mcp.ts` L863. - `packages/mcp/src/mcp-http-tools.ts` L395-397 `isListableAction`, with the refusal text at L399-404. The `list_actions` filter is at L1483 and the `run_action` pre-screen at L1585-1590. - `packages/platform-objects/src/identity/sys-api-key.object.ts`: `create_api_key` (L100) has no `ai` block, and `revoke_api_key` (L142) declares `ai.exposed` with `requiresConfirmation: true`. <details><summary>Unit pins run at dd68162</summary> - `pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2 src/mcp-action-system-object-exposure.test.ts` (under the verify lock, after building the `@objectstack/mcp^...` closure): `Test Files 1 passed (1)` · `Tests 8 passed (8)` · `VERDICT command-exit 0`. - `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/mcp-system-object-action-exposure.test.ts` (after building `@objectstack/runtime^...`): `Test Files 1 passed (1)` · `Tests 11 passed (11)` · `VERDICT command-exit 0`. - Live corroboration, from #29892 at `589049b0` and not re-run here: `create_api_key` was refused in both spellings, and `revoke_api_key` with `confirm: true` under the admin session answered `ok: true`. </details> ## Acceptance notes - The new step and clause have not been run live. The first sweep that runs revision 7 proves the recipe. - `node scripts/check-platform-checklist.mjs` exits 0. ## Verification <details><summary>Derived gates at dd68162 (16 derived, 16 run, 0 NOT-MEASURED)</summary> Every command `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives exited 0: `check-adr-links` (plus `--self-test`), `check-closing-keyword-parity` (plus `--self-test`), `check-comment-mask-corpus`, `plan-test-core-shards --self-test`, `check:doc-formula-expressions`, `check:ci-filter-parity`, `check:cross-package-test-inputs`, `check:doc-authoring`, `check:driver-memory-census`, `check:gitlink-declared`, `check:nul-bytes`, `check:platform-checklist`, `check:refd-timer-probe` and `check:watch-hint-literal`. The first `check:doc-formula-expressions` run exited 3 (PREREQUISITE NOT MET: unbuilt `@objectstack/formula` and `@objectstack/lint`). After `turbo run build` of those two packages it exited 0. The `--ran` reconciliation reported 16 derived, 16 run and 0 NOT-MEASURED, a derived zero because every line recorded its exit code. </details> `origin/main` was merged once before opening this PR; it had not moved past `a04e9d76c1`. The label is `skip-changeset` because the diff is docs only and publishes nothing. Session: https://claude.ai/code/session_017ihL2gLghEqeDKTbDTC7Zd --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4c2cd29 commit 242665f

1 file changed

Lines changed: 15 additions & 10 deletions

File tree

  • docs/qa/platform-checklist/areas

‎docs/qa/platform-checklist/areas/ai.json‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,10 @@
370370
},
371371
{
372372
"id": "ai.mcp-run-action-exposure-gate",
373-
"title": "MCP run_action requires ai.exposed (fail-closed with the prescription), filters list_actions to exposed+permitted, refuses system objects, and logs the trusted-elevation audit line",
373+
"title": "MCP run_action requires ai.exposed (fail-closed with the prescription), filters list_actions to exposed+permitted, admits a system-object action only on its own ai.exposed, and logs the trusted-elevation audit line",
374374
"since": "v15.1",
375375
"status": "active",
376-
"revision": 6,
376+
"revision": 7,
377377
"priority": "P1",
378378
"surface": "api",
379379
"fixtures": {
@@ -382,7 +382,8 @@
382382
"the showcase's ONE ai-exposed action: showcase_portfolio_snapshot (global script action, ai: { exposed: true }, examples/app-showcase/src/ui/actions/index.ts) — the exposed side",
383383
"any showcase action WITHOUT an ai block (e.g. showcase_mark_done on showcase_task) — the undeclared side",
384384
"MCP reachable with an osk_ key (see ai.mcp-http-surface)",
385-
"acceptance[1]'s PERMITTED half has no stock producer: showcase_portfolio_snapshot gates no permission, so nothing exposed is ever held back. Stage one at runtime as the admin: PUT /api/v1/meta/action/qa_ai_gated { name: 'qa_ai_gated', label: 'QA AI gated', objectName: 'showcase_task', operation: 'update', patch: { progress: 50 }, execution: 'perRecord', ai: { exposed: true, description: 'QA probe: an AI-exposed action behind a capability gate' }, requiredPermissions: ['manage_metadata'] }. It is a DECLARATIVE update because the listing drops a script action with no registered handler before it asks the permission (listActions, packages/runtime/src/domains/mcp.ts), and it requires manage_metadata because the admin holds that capability and a member persona does not (actionPermissionError, packages/runtime/src/action-execution.ts). The member side needs an osk_ key of a member persona (Mei Phone, phone.demo@example.com), minted as ai.mcp-http-surface mints the admin's. DELETE /api/v1/meta/action/qa_ai_gated in teardown"
385+
"acceptance[1]'s PERMITTED half has no stock producer: showcase_portfolio_snapshot gates no permission, so nothing exposed is ever held back. Stage one at runtime as the admin: PUT /api/v1/meta/action/qa_ai_gated { name: 'qa_ai_gated', label: 'QA AI gated', objectName: 'showcase_task', operation: 'update', patch: { progress: 50 }, execution: 'perRecord', ai: { exposed: true, description: 'QA probe: an AI-exposed action behind a capability gate' }, requiredPermissions: ['manage_metadata'] }. It is a DECLARATIVE update because the listing drops a script action with no registered handler before it asks the permission (listActions, packages/runtime/src/domains/mcp.ts), and it requires manage_metadata because the admin holds that capability and a member persona does not (actionPermissionError, packages/runtime/src/action-execution.ts). The member side needs an osk_ key of a member persona (Mei Phone, phone.demo@example.com), minted as ai.mcp-http-surface mints the admin's. DELETE /api/v1/meta/action/qa_ai_gated in teardown",
386+
"acceptance[4]'s two system-object actions, both on sys_api_key (packages/platform-objects/src/identity/sys-api-key.object.ts): the REFUSED side is create_api_key, which declares no ai block and, as a credential-minting action, is held human-only by ruling 6513869839 (#27681, parameter 2); the EXPOSED side is revoke_api_key, declared ai: { exposed: true, requiresConfirmation: true } by ruling 6523087116 (#27900, bucket C). The exposed run needs a probe key to revoke: as the admin, POST /api/v1/keys { name: 'qa-revoke-probe' } and keep its id. It also needs the admin's SESSION token as the MCP bearer (the token field of POST /api/v1/auth/sign-in/email's answer), because under an osk_ key revoke_api_key is listed with unavailable and refused by a later gate (apiForwardCredentialRefusal, packages/runtime/src/action-execution.ts), never reaching its result. The probe key stays revoked after the run"
386387
]
387388
},
388389
"steps": [
@@ -392,7 +393,7 @@
392393
"verify the refusal left zero side effects: read the task over REST — status unchanged",
393394
"call run_action on the exposed action (actionName: 'showcase_portfolio_snapshot'); capture the result payload ({ ok, scope: 'global', accounts, projects, invoices })",
394395
"capture the server log for the successful call",
395-
"call run_action against an action on a system object (any sys_* action name) and capture that refusal",
396+
"system-object actions, both halves (fixtures.requires[4]): REFUSED half: read the sys_api_key row count over REST, then with the osk_ key call run_action on the non-exposed create_api_key twice, once with objectName: 'sys_api_key' (params: { name: 'qa-must-not-exist' }) and once without objectName; capture both refusals, re-read the count, and confirm create_api_key had no entry in steps[0]'s list_actions. EXPOSED half: confirm steps[0]'s list_actions carries revoke_api_key on sys_api_key with aiExposed: true, then with the admin's session token as the MCP bearer call run_action { actionName: 'revoke_api_key', objectName: 'sys_api_key', recordId: the probe key's id, confirm: true }; capture the result and re-read the probe key over REST",
396397
"cross-check the exposed action's counts against direct REST counts of showcase_account / showcase_project / showcase_invoice"
397398
],
398399
"acceptance": [
@@ -405,7 +406,7 @@
405406
{
406407
"clause": "list_actions returns ONLY actions that are both ai-exposed and permitted to the caller — showcase_portfolio_snapshot present, showcase_mark_done absent",
407408
"oracle": "api",
408-
"verify": "the list_actions payload against the fixture's declared ai blocks (mcp-http-tools.ts: 'actions that are BOTH declared ai-exposed and that the caller is permitted to run'). On the stock showcase only the ai-exposed half discriminates: showcase_portfolio_snapshot is the one exposed action and no persona lacks a permission it needs, so no exposed-but-unpermitted action exists to be held back. The staged qa_ai_gated action (fixtures.requires[3]) discriminates it: present in the admin's list_actions, absent from the member's, while showcase_portfolio_snapshot is in both. Without that staging, score the permitted half not-run(fixture)",
409+
"verify": "the list_actions payload against the fixture's declared ai blocks (mcp-http-tools.ts: 'actions that are BOTH declared ai-exposed and that the caller is permitted to run'). The list also carries the platform's ai-exposed sys_* identity actions, each listed on its own ai.exposed (isListableAction, packages/mcp/src/mcp-http-tools.ts; 36 ai: { exposed: true } blocks under packages/platform-objects/src/identity at a04e9d76; #29892 read 37 entries on a stock boot at 589049b0), so showcase_portfolio_snapshot is the showcase's one app-authored exposed action, not the list's only entry. An entry that carries unavailable (code + reason) is listed by contract for a credential its target cannot accept, not a FAIL of this clause. On the stock showcase only the ai-exposed half discriminates: neither showcase_portfolio_snapshot nor any exposed sys_* action declares requiredPermissions, so no persona lacks a permission one needs and no exposed-but-unpermitted action exists to be held back. The staged qa_ai_gated action (fixtures.requires[3]) discriminates it: present in the admin's list_actions, absent from the member's, while showcase_portfolio_snapshot is in both. Without that staging, score the permitted half not-run(fixture)",
409410
"evidence": "the list_actions response"
410411
},
411412
{
@@ -421,10 +422,10 @@
421422
"evidence": "the log excerpt"
422423
},
423424
{
424-
"clause": "system objects are refused wholesale: run_action against a sys_* object errors with the system-object guard in either of its two spellings — with objectName the MCP tool layer answers 'Object \"sys_x\" is a system object and its actions are not exposed via MCP' (mcp-http-tools.ts); without it the runtime guard answers \"Action '<n>' is on a system object and is not exposed via MCP\" (action-execution.ts) — and never executes",
425+
"clause": "a system-object action passes the MCP system-object guard only on its OWN ai.exposed declaration, never by its object's name. REFUSED: create_api_key on sys_api_key (no ai block) is absent from list_actions, and run_action refuses it in either of the guard's two spellings — with objectName the MCP tool layer answers 'Action \"create_api_key\" on system object \"sys_api_key\" is not exposed via MCP: a system object's action is reachable only when its author exposed it to AI (ai.exposed) and list_actions lists it for you'; without it the runtime guard answers \"Action 'create_api_key' is on a system object ('sys_api_key') and is not exposed via MCP — a system object's action is reachable here only when its author declares `ai: { exposed: true, description: … }` on it\" — and no key is created. EXPOSED: revoke_api_key on sys_api_key (ai: { exposed: true, requiresConfirmation: true }) is listed with aiExposed: true and passes the guard: called with the admin's session token and confirm: true it answers ok: true and the probe key reads revoked: true",
425426
"oracle": "api",
426-
"verify": "the refusal text + absence of any side effect",
427-
"evidence": "the refusal"
427+
"verify": "the refused half's two texts against systemObjectActionRefusal (packages/mcp/src/mcp-http-tools.ts) and systemObjectActionExposureError (packages/runtime/src/action-execution.ts), and the sys_api_key row count unchanged across both calls; create_api_key absent from and revoke_api_key present in the list_actions payload with aiExposed: true (isListableAction, packages/mcp/src/mcp-http-tools.ts; the runtime listing asks the same systemObjectActionExposureError, packages/runtime/src/domains/mcp.ts listActions); the exposed run's result and GET /api/v1/data/sys_api_key/{id} reading revoked: true. A sys_* action with no ai block that is listed or runs, or an ai.exposed sys_* action refused with either system-object text, is a FAIL. Under the osk_ key revoke_api_key is listed with unavailable and refused by its target's credential gate, never by this guard; that agreement is negative[1]'s, not this clause's. Rulings: 6513869839 (#27681, letter B) and 6523087116 / 6540309323 (#27900, the ADR-0011 identity-actions addendum)",
428+
"evidence": "both refusals + the before/after sys_api_key counts + the two list_actions entries (create_api_key absent, revoke_api_key present) + the exposed run's result + the probe key re-read"
428429
}
429430
],
430431
"negative": [
@@ -436,7 +437,10 @@
436437
"packages/runtime/src/action-execution.ts#actionAiExposureError (actionAiExposureError exact string; system-object guard; [action-audit] line; shared REST/MCP gate #3915)",
437438
"packages/mcp/src/mcp-http-tools.ts#list_actions (list_actions/run_action registration + exposed-and-permitted filter)",
438439
"examples/app-showcase/src/ui/actions/index.ts#showcase_portfolio_snapshot (showcase_portfolio_snapshot — the seeded ai.exposed fixture)",
439-
"docs/plans/release-15.1-test-plan.md §A9 (#2964) + §G2 (#3010/#3020 standalone action on the bridge)"
440+
"docs/plans/release-15.1-test-plan.md §A9 (#2964) + §G2 (#3010/#3020 standalone action on the bridge)",
441+
"packages/runtime/src/action-execution.ts#systemObjectActionExposureError (the per-action sys_* gate run_action and the list_actions bridge both ask; yields to ai.exposed only)",
442+
"packages/mcp/src/mcp-http-tools.ts#isListableAction (the tool layer's sys_* listing and run_action pre-screen; systemObjectActionRefusal's text)",
443+
"packages/platform-objects/src/identity/sys-api-key.object.ts#revoke_api_key (create_api_key with no ai block, revoke_api_key ai-exposed: acceptance[4]'s two halves)"
440444
],
441445
"history": [
442446
{ "revision": 1, "date": "2026-08-07", "change": "initial import from the 15.1 plan §A9", "ref": "claude/platform-test-checklist-ocwugl" },
@@ -449,7 +453,8 @@
449453
"change": "acceptance[1].verify says which half the stock fixture discriminates. Checklist-accuracy finding of the 18.2.0 wave 2 (QA run #26476, manual api set, at 756220c4; wave anchor #25807, slice B): the clause's 'permitted' filter was not discriminated, because no stock action pairs ai.exposed with a permission gate a persona lacks. Re-read at base fd246c22: examples/app-showcase/src/ui/actions/index.ts carries one ai: { exposed: true } block (showcase_portfolio_snapshot). The clause is unchanged; the verify names the fixture gap",
450454
"ref": "#25807"
451455
},
452-
{ "revision": 6, "date": "2026-10-31", "change": "fixtures.requires[3], steps[1] and acceptance[1].verify stage the permitted half of the list_actions filter. Fixture gap of QA run #27392 (18.3.0 sweep, wave 3, at c4c515ad), parked on sweep anchor #27377: the permitted half scored blocked(fixture) because no stock action pairs ai.exposed with a permission some persona lacks. The recipe is a runtime declarative update action, ai-exposed, requiring manage_metadata, listed for the admin and held back from a member. Derived from source at base 488b3ed0: packages/runtime/src/domains/mcp.ts listActions (the handler check precedes the permission check, which a declarative update skips), packages/runtime/src/action-execution.ts actionPermissionError, packages/spec/src/ui/action.zod.ts (ai.exposed requires a description). Not yet run live: the first run that stages it proves the recipe. The clause is unchanged", "ref": "#27392" }
456+
{ "revision": 6, "date": "2026-10-31", "change": "fixtures.requires[3], steps[1] and acceptance[1].verify stage the permitted half of the list_actions filter. Fixture gap of QA run #27392 (18.3.0 sweep, wave 3, at c4c515ad), parked on sweep anchor #27377: the permitted half scored blocked(fixture) because no stock action pairs ai.exposed with a permission some persona lacks. The recipe is a runtime declarative update action, ai-exposed, requiring manage_metadata, listed for the admin and held back from a member. Derived from source at base 488b3ed0: packages/runtime/src/domains/mcp.ts listActions (the handler check precedes the permission check, which a declarative update skips), packages/runtime/src/action-execution.ts actionPermissionError, packages/spec/src/ui/action.zod.ts (ai.exposed requires a description). Not yet run live: the first run that stages it proves the recipe. The clause is unchanged", "ref": "#27392" },
457+
{ "revision": 7, "date": "2026-11-03", "change": "acceptance[4] and steps[6] re-pointed from the retired wholesale sys_* refusal to the per-action gate as ruled: a sys_* action is listed and runs over MCP only when the action itself declares ai.exposed, and is refused otherwise (ruling 6513869839 on #27681, letter B; the ADR-0011 identity-actions addendum, rulings 6523087116 and 6540309323 on #27900). Stale-spec card #29892, a QA clause fail of waves 4 and 5 of the 18.3.0 sweep (QA run #29890). The clause keeps both halves: the non-exposed create_api_key refused in both live spellings with no key created, and the ai-exposed revoke_api_key listed and run under a session token. steps[6] names those two actions, since any sys_* action name can now pick an exposed one; fixtures.requires[4] stages the probe key and the session bearer. Riders from the same rulings: the title no longer says system objects are refused, and acceptance[1].verify no longer calls showcase_portfolio_snapshot the one exposed action (the listing carries the ai-exposed sys_* identity actions too, and an unavailable entry is listed by contract). Derived from source at base a04e9d76: packages/runtime/src/action-execution.ts systemObjectActionExposureError, packages/mcp/src/mcp-http-tools.ts isListableAction and systemObjectActionRefusal, packages/runtime/src/domains/mcp.ts listActions, packages/platform-objects/src/identity/sys-api-key.object.ts. Behaviour as read on a stock boot by #29892 at 589049b0, not re-run live here. negative[1] (the #28943 list/run disagreement) is unchanged", "ref": "#29892" }
453458
]
454459
},
455460
{

0 commit comments

Comments
 (0)