|
370 | 370 | }, |
371 | 371 | { |
372 | 372 | "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", |
374 | 374 | "since": "v15.1", |
375 | 375 | "status": "active", |
376 | | - "revision": 6, |
| 376 | + "revision": 7, |
377 | 377 | "priority": "P1", |
378 | 378 | "surface": "api", |
379 | 379 | "fixtures": { |
|
382 | 382 | "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", |
383 | 383 | "any showcase action WITHOUT an ai block (e.g. showcase_mark_done on showcase_task) — the undeclared side", |
384 | 384 | "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" |
386 | 387 | ] |
387 | 388 | }, |
388 | 389 | "steps": [ |
|
392 | 393 | "verify the refusal left zero side effects: read the task over REST — status unchanged", |
393 | 394 | "call run_action on the exposed action (actionName: 'showcase_portfolio_snapshot'); capture the result payload ({ ok, scope: 'global', accounts, projects, invoices })", |
394 | 395 | "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", |
396 | 397 | "cross-check the exposed action's counts against direct REST counts of showcase_account / showcase_project / showcase_invoice" |
397 | 398 | ], |
398 | 399 | "acceptance": [ |
|
405 | 406 | { |
406 | 407 | "clause": "list_actions returns ONLY actions that are both ai-exposed and permitted to the caller — showcase_portfolio_snapshot present, showcase_mark_done absent", |
407 | 408 | "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)", |
409 | 410 | "evidence": "the list_actions response" |
410 | 411 | }, |
411 | 412 | { |
|
421 | 422 | "evidence": "the log excerpt" |
422 | 423 | }, |
423 | 424 | { |
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", |
425 | 426 | "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" |
428 | 429 | } |
429 | 430 | ], |
430 | 431 | "negative": [ |
|
436 | 437 | "packages/runtime/src/action-execution.ts#actionAiExposureError (actionAiExposureError exact string; system-object guard; [action-audit] line; shared REST/MCP gate #3915)", |
437 | 438 | "packages/mcp/src/mcp-http-tools.ts#list_actions (list_actions/run_action registration + exposed-and-permitted filter)", |
438 | 439 | "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)" |
440 | 444 | ], |
441 | 445 | "history": [ |
442 | 446 | { "revision": 1, "date": "2026-08-07", "change": "initial import from the 15.1 plan §A9", "ref": "claude/platform-test-checklist-ocwugl" }, |
|
449 | 453 | "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", |
450 | 454 | "ref": "#25807" |
451 | 455 | }, |
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" } |
453 | 458 | ] |
454 | 459 | }, |
455 | 460 | { |
|
0 commit comments