From 6aa5ec3ea71f1a8baa7a6f977c64790fd8f26ab0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 12:27:54 +0000 Subject: [PATCH] docs(adr-0066): record that D4's three landing steps and the sys_* MCP follow-up have landed ADR-0066 D4's 2026-10-29 amendment said api actions on sys_* objects stay off MCP (card #27681) and that the console still fetches an api action's target directly. PR #27940 closed the first under ruling 6513869839 (per action ai.exposed, the stored-metadata family refused before the flag), and PR #27793 closed the second for #27611. A dated note under the amendment records both, plus step 2 (PR #27649), with symbol anchors at the merge commits; the Status line records the landings. The requiredPermissions JSDoc's "Landing, as of 2026-10-29" paragraph had nothing left to schedule, so it goes. No .describe() change. Fixes #27694 Fixes #27956 Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cw9gAhrah1Mtf9tSZ7 --- docs/adr/0066-unified-authorization-model.md | 4 +++- packages/spec/src/ui/action.zod.ts | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/adr/0066-unified-authorization-model.md b/docs/adr/0066-unified-authorization-model.md index c7f51676bee..264f5693a96 100644 --- a/docs/adr/0066-unified-authorization-model.md +++ b/docs/adr/0066-unified-authorization-model.md @@ -1,6 +1,6 @@ # ADR-0066: Unified authorization model — capability registry, secure-by-default posture, resource→capability contracts, dual-surface gates -**Status**: Accepted (2026-06-23) — D1–D5 + refinements ⑤/⑨ implemented (capability registry + `defineCapability` seeding, `access.default` posture with posture-gated superuser bypass, `requiredPermissions` AND-gates on object/field/action, `crudBucketForOperation`, `validateCapabilityReferences` lint); D4's UI half enforced in objectui ActionRunner. · **Amended** (2026-10-14, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D2/D3/D13): the stored records this record names — `sys_permission_set` and `sys_capability` — are retired as row objects by #15204 stage 8 (`66949b7729`, 2026-10-13), and the seeders that filled them are retired under ADR-0131 D2; permission sets and capabilities are metadata-registry definitions, and the `sys_user_permission_set` assignment stays. The three-way separation stands; see the notes under the principle, D1, D5 and ⑨. · **Amended** (2026-10-29, ruling of record 6499781492 on #27506 — maintainer, verbatim: 「我们的平台设计目标是 agent 和人类使用同一套系统」「同意」): the browser-only half of D4's 2026-07 api clause is superseded — a `type: 'api'` action whose `target` is a same-origin platform path is dispatched by the platform action door (`POST /api/v1/actions//` and MCP `run_action`), which runs the D4 gate before forwarding; the `target` endpoint stays directly reachable and must still enforce the declared capability itself, the door's gate being the added check; an off-origin api action stays browser-only. The server half lands under #27506; see the note under D4. +**Status**: Accepted (2026-06-23) — D1–D5 + refinements ⑤/⑨ implemented (capability registry + `defineCapability` seeding, `access.default` posture with posture-gated superuser bypass, `requiredPermissions` AND-gates on object/field/action, `crudBucketForOperation`, `validateCapabilityReferences` lint); D4's UI half enforced in objectui ActionRunner. · **Amended** (2026-10-14, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D2/D3/D13): the stored records this record names — `sys_permission_set` and `sys_capability` — are retired as row objects by #15204 stage 8 (`66949b7729`, 2026-10-13), and the seeders that filled them are retired under ADR-0131 D2; permission sets and capabilities are metadata-registry definitions, and the `sys_user_permission_set` assignment stays. The three-way separation stands; see the notes under the principle, D1, D5 and ⑨. · **Amended** (2026-10-29, ruling of record 6499781492 on #27506 — maintainer, verbatim: 「我们的平台设计目标是 agent 和人类使用同一套系统」「同意」): the browser-only half of D4's 2026-07 api clause is superseded — a `type: 'api'` action whose `target` is a same-origin platform path is dispatched by the platform action door (`POST /api/v1/actions//` and MCP `run_action`), which runs the D4 gate before forwarding; the `target` endpoint stays directly reachable and must still enforce the declared capability itself, the door's gate being the added check; an off-origin api action stays browser-only. The server half lands under #27506; see the note under D4. Landed: the server half by PR #27649 (2026-10-29); step 3, the console, by PR #27793 for #27611 (2026-10-30); the `sys_*` MCP follow-up by PR #27940 for #27681 (2026-10-30, ruling 6513869839) — see the 2026-10-30 note under D4. **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`readScope`/`writeScope` depth: own/unit/unit_and_below/org), [ADR-0058](./0058-expression-and-predicate-surface.md) (CEL predicate surface used by RLS); relates to cloud ADR-0016 (authz open/paid boundary) **Consumers**: `@objectstack/spec` (object/app/permission schemas), `@objectstack/plugin-security` (RLS/FLS compiler + enforcement), `../objectui` (ActionRunner + app/nav gating), `../cloud` (control-plane objects, e.g. `sys_license`) @@ -74,6 +74,8 @@ An action declaring `requiredPermissions` is enforced in **one place, two surfac **Scope of the server half (clarified 2026-07, framework#3923; api clause amended 2026-10-29, Ruling-ref: 6499781492).** "The server rejects the call" means the PLATFORM ACTION DOOR — `POST /api/v1/actions//` and the MCP `run_action` bridge, which enforce one gate (`packages/runtime/src/action-execution.ts#actionPermissionError`) and dispatch `type: 'script'`, `type: 'flow'` and the declarative `operation: 'update'` write. A `type: 'api'` action whose `target` is a same-origin platform path — the declared `target`, before interpolation, starts with exactly one `/` and goes on past it (`/` alone, `//host` and `/\host` are not), as `packages/runtime/src/action-execution.ts#isSameOriginApiTarget` judges it (PR #27649; the door holds the interpolated path to the same test before it forwards) — is dispatched by that door too: the route and the bridge run the anonymous baseline, this D4 gate and the ADR-0126 §8 activation consult, then forward the call in-process to the declared `target` as the caller — the request assembled from the declaration (`method`, `recordIdParam`, `bodyShape`, `bodyExtra`, the placeholders in `target`), the caller's cookies and auth headers carried. The target's `Set-Cookie` is returned on the REST route's response; the MCP bridge relays only the bearer-lane rotation headers (`set-auth-token`, `set-admin-session-token`), and no cookie value enters a tool result. The door gates the calls it dispatches, and only those: the `target` endpoint stays directly reachable — nothing routes a direct call to it through the door — so it MUST still enforce the declared capability itself, and the door's gate is the added check: defence in depth, not a replacement. An api action whose `target` is not a same-origin platform path stays browser-only and ungoverned: the door does not dispatch it, the MCP bridge does not list it, and the activation write door (`POST /api/v1/actions/_activation//`, `packages/runtime/src/domains/actions.ts#handleActionActivationWrite`) refuses to flip it, with a reason, rather than record a switch nothing consults. The UI half is correspondingly *every* action surface, not just the engine: `record_header`/`record_more` (`page:header`) and `list_item` (the grid row menu) filter their own action lists and were fixed to apply the same gate (objectui), alongside `ActionEngine.getActionsForLocation`. All of them fail OPEN when the client does not know the caller's capabilities — unknown is not denied. > **Amended (2026-10-29) — the api clause above reverses the browser-only half of the 2026-07 clarification (ruling of record 6499781492 on #27506; Prime Directive #13: a recorded decision is reversed by an ADR edit, never by a changeset).** As clarified on 2026-07 (framework#3923) this paragraph read: "A `type: 'api'` action whose `target` is a self-authored endpoint is fetched by the browser directly and never passes through that route, so the declaration has no server-side consumer there; that endpoint must re-check the capability itself." Its first half — fetched by the browser directly, never passes through that route, no server-side consumer — recorded the gap as the rule and is superseded, not deleted. Its last clause stands: the endpoint must still re-check the capability itself, now beside the door's gate (the paragraph above). Under the maintainer's design goal, a capability a human reaches by clicking and an agent cannot reach through the action surface is a defect, and the unhonoured ADR-0126 switch on a `type: 'api'` action is one symptom of it. The maintainer, verbatim and untranslated: 「我们的平台设计目标是 agent 和人类使用同一套系统」 / 「同意」. The paragraph above also drops `modal` from the dispatched types: the door answers a `modal` action 400 as a client-side type (`packages/runtime/src/action-execution.ts#headlessActionTypeError`), and it has since gained the declarative update. **Landing, in three steps.** This record is step 1. Step 2 is the server card #27506: the door and the bridge accept a declared same-origin `type: 'api'` action, `packages/runtime/src/action-execution.ts#isHeadlessInvokableAction` admits it (and the `list_actions` filter in `packages/runtime/src/domains/mcp.ts#buildMcpBridge` is widened beside it), and the activation write door refuses an off-origin one. That does not put every api action in front of an agent through MCP: `list_actions` and `run_action` keep the `ai.exposed` opt-in and the fail-closed `sys_*` guard (`packages/runtime/src/domains/mcp.ts#buildMcpBridge` for the listing, `packages/runtime/src/action-execution.ts#invokeBusinessAction` for the call, both through `packages/runtime/src/action-execution.ts#isSystemObjectName`), so `sys_user.ban_user` and the other api actions on platform `sys_*` objects stay off MCP and reach an agent through REST `POST /api/v1/actions//` — a gap this amendment records and does not close; card #27681 carries it. Step 3 is the ui card #27611: the console's api handlers send a same-origin click to the door instead of fetching `target`. Until step 2 lands the door still refuses `type: 'api'` — `packages/runtime/src/action-execution.ts#isHeadlessInvokableAction` admits `script`, `flow` and the declarative update only, `packages/runtime/src/action-execution.ts#SERVER_DISPATCHED_ACTION_TYPES` is `script`, `flow`, and `packages/runtime/src/action-execution.ts#headlessActionTypeError` answers an api action 400 — so no agent can invoke a `type: 'api'` action today, and the console still fetches `target` directly. +> +> **Note (2026-10-30, #27956, #27694) — all three steps have landed, and the `sys_*` gap is closed.** The amendment above reads as of its date; this note records what landed after it and changes no decision. Its closing sentence ("Until step 2 lands the door still refuses `type: 'api'` … and the console still fetches `target` directly") and its "stay off MCP … a gap this amendment records and does not close; card #27681 carries it" are superseded by this note, not deleted. (a) **The `sys_*` MCP gap is closed** by PR #27940 (#27681, maintainer ruling of record 6513869839, letter B). The rule is now per action: an action on a `sys_*` object is listed and run over MCP only when the action itself declares `ai.exposed: true` (ADR-0011); no deployment switch opens one. One predicate, `packages/runtime/src/action-execution.ts#systemObjectActionExposureError`, answers for the three MCP sites — `run_action` (`packages/runtime/src/action-execution.ts#invokeBusinessAction`), the `list_actions` filter (`packages/runtime/src/domains/mcp.ts#buildMcpBridge`) and the `resume_run` admission (`packages/runtime/src/domains/mcp.ts#resumeAdmissionRefusal`) — and the MCP tool layer's fail-closed filter, with its `allowSystemObjects` option at the default off, admits a `sys_*` action only on the bridge's attestation of that opt-in (`packages/mcp/src/mcp-http-tools.ts#isListableAction`). `packages/runtime/src/action-execution.ts#isSystemObjectName` still decides which objects the rule applies to. The stored-metadata body family (`sys_metadata`, `sys_metadata_history`, `packages/spec/src/kernel/stored-metadata-body-objects.ts#isStoredMetadataBodyObject`) is refused before the flag is read, so no declaration opens it. Every gate after admission is unchanged: `packages/runtime/src/action-execution.ts#actionAiExposureError`, this D4 gate (`packages/runtime/src/action-execution.ts#actionPermissionError`), the ADR-0126 §8 activation consult, the param contract, the `ai.requiresConfirmation` gate and the subject load under RLS. (b) **Step 2 landed** by PR #27649 (2026-10-29): the door and the bridge dispatch a declared same-origin `type: 'api'` action, as the D4 paragraph above states. (c) **Step 3 landed** by PR #27793 for #27611 (2026-10-30): the console sends a click on a declared same-origin `type: 'api'` action to `POST /api/v1/actions//` instead of fetching `target`. A click whose `target` is off-origin, and one that resolves to no declared action in the object metadata the console holds (an inline page button that names none, an object-less action), still fetches `target` directly. ### D5 — Package-seeded, admin-maintainable policies [existing mechanism, fill the gap] A package may seed permission-set policies (incl. per-object grants for **its own** objects) via `stack.permissions` **[existing]**; these land as `sys_permission_set` records **admins can edit in Setup** **[existing]**. The gap to close: make a package's per-object secure default (D2 + an admin-only grant) expressible + seedable so a sensitive package object is locked on install and tunable thereafter. diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index d4624d14d35..2c94d856d73 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -1610,11 +1610,6 @@ const actionObject = () => strictObject({ * and ungoverned — the door does not dispatch it, MCP does not list it, the * activation switch refuses to flip it, and nothing checks this declaration * server-side. - * - * Landing, as of 2026-10-29: ADR-0066 records the decision; the server half, - * the ruling's step 2 (#27506), is on `main` (#27649). Until step 3 (#27611) - * lands the console fetches `target` directly; card #27694 removes this - * paragraph when it does. */ requiredPermissions: z.array(z.string()).optional().describe('[ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on every call the platform action door (`POST /actions/:object/:action` and MCP `run_action`) dispatches, and mirrored as a UI hide. ADR-0066 D4, as amended 2026-10-29, brings a `type: api` action whose declared `target` is a same-origin platform path (exactly one leading `/`, followed by neither `/` nor a backslash, with a path after it) under that door; its `target` endpoint stays directly reachable, so it MUST still enforce this capability itself, the door\'s gate being the added check. A `type: api` action with an off-origin `target` is browser-only and not gated server-side.'),