Skip to content

Commit 75b0c8b

Browse files
docs(adr-0066): record that D4's three landing steps and the sys_* MCP follow-up have landed (#28119)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DRAvoKK4WGPwFkh7crrTU5R)_ Fixes #27694 Fixes #27956 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:ADR-0066 D4 的 2026-10-29 修订下面加一条带日期的注记(2026-10-30),记录三件已落地的事:`sys_*` 对象上的动作经 MCP 的缺口已由 #27940 关闭(按动作逐条看 `ai.exposed: true`,存储元数据表照旧在读开关前就拒绝,后面各闸不变);第 2 步 #27649 已落地;第 3 步 #27611(PR #27793)已落地。原句不改,标为「按当日读,被本注记取代」。Status 行补一句落地记录。`requiredPermissions` 文档注释里的「Landing, as of 2026-10-29」整段删掉。 - 为什么改:ADR 还写着「sys_* 动作留在 MCP 之外、#27681 承接」和「控制台仍直接请求 `target`」,两句在 main 上都已不成立;随 `@objectstack/spec` 的 `.d.ts` 发出的注释还在说控制台条款和「#27694 会删掉本段」。治理文本说错,等于给作者和 agent 讲错规则。 - 风险与回滚:两个文件 +3/−6,只动 ADR 正文和一段 JSDoc 注释;不改 `.describe()`、不改代码、无生成物变化、不发布(`skip-changeset`)。回滚即 revert 这一个提交。 - 先看:`docs/adr/0066-unified-authorization-model.md` D4 修订段下面新加的 `> **Note (2026-10-30, #27956, #27694)**` 那一段,再看第 3 行 Status 末尾新加的「Landed: …」一句。 - 两分钟验证:`pnpm check:adr-symbol-anchors`,预期 exit 0,符号锚 623 个(597 个声明锚),main 上是 614(588);多出的 9 个就是注记里引用的 9 个符号,以后改名或挪走会转红。 - 需要你做的:暂无。contract review owed(契约复审未完成,席位复审后再求批);落地档以登记表在 PR 上的读数为准,预期 Tier H,需受管批准人(os-zhuang 或 hotlong)批准。 ## What changes Two files, +3 / −6, one commit on `origin/main` `6b125a7a`. - **`docs/adr/0066-unified-authorization-model.md`, D4.** A dated note, `Note (2026-10-30, #27956, #27694)`, goes under the 2026-10-29 amendment paragraph, in the ADR's own note style (Prime Directive #13: the recorded sentence stays and a note goes under it). It says the amendment reads as of its date, and that two of its clauses are superseded by the note, not deleted: the closing "Until step 2 lands …" sentence, and "stay off MCP … card #27681 carries it". Then it records: - (a) the `sys_*` MCP gap is closed by PR #27940 (#27681, ruling `6513869839`, letter B). The rule is per action: a `sys_*` action is listed and run over MCP only when it declares `ai.exposed: true` itself. One predicate (`systemObjectActionExposureError`) answers for `run_action`, the `list_actions` filter and the `resume_run` admission, and the tool layer's filter yields only to the bridge's attestation (`isListableAction`). The stored-metadata family is refused before the flag is read (`isStoredMetadataBodyObject`). Every gate after admission is unchanged; - (b) step 2 landed (PR #27649, 2026-10-29); - (c) step 3 landed (PR #27793 for #27611, 2026-10-30). The off-origin and undeclared/object-less clicks still fetch `target` directly. - **ADR-0066, Status line.** After the recorded "The server half lands under #27506; see the note under D4.", one sentence records the landings: server half PR #27649 (2026-10-29), step 3 PR #27793 for #27611 (2026-10-30), the `sys_*` follow-up PR #27940 for #27681 (2026-10-30, ruling `6513869839`). This is should-fix 1 of the first-pass review 6515866455. - **`packages/spec/src/ui/action.zod.ts`, the `requiredPermissions` docblock.** The "Landing, as of 2026-10-29" paragraph is removed. Once the console clause and the self-removal sentence go, what is left ("ADR-0066 records the decision; the server half … is on `main`") is a landing status with nothing pending. The paragraph above it already cites ADR-0066 D4 as amended 2026-10-29. No `.describe()` change. ## Why The amendment's present-tense clauses are false on `main`: - `invokeBusinessAction`, the `list_actions` bridge (`buildMcpBridge`) and `resumeAdmissionRefusal` all call `systemObjectActionExposureError` (bc496bf). It returns `null` for a `sys_*` action that declares `ai.exposed: true`. It refuses `sys_metadata` / `sys_metadata_history` before reading the flag. `isSystemObjectName` is still its first test, so the amendment's citation of it still resolves and stays true as the "which objects" test. - `useConsoleActionRuntime` and `RecordDetailView` send a declared same-origin api click to `POST /api/v1/actions/:object/:action` through `ui/packages/app-shell/src/utils/apiActionDoor.ts` (b00e3f4). An off-origin target, an inline button that names no declared action, and an object-less action keep the direct fetch, as that module's header says. ## Verification - `check:adr-symbol-anchors`: exit 0. Head: 623 symbol anchors (597 declaration, 26 literal), 2163 file-level. Base `6b125a7a`: 614 (588, 26), 2163. The +9 declaration anchors are the nine new citations; every one resolves. - `node scripts/pm/dispatch-gates.mjs --commands`: 74 families derived after the commit. All 74 exit 0. `--ran` reconcile: 74 run, 0 NOT-MEASURED, 0 UNRUN. That is after a whole-tree `turbo run build` (116/116), because 15 families exit 3 or miss generated inputs in a fresh worktree. - `pnpm --filter @objectstack/spec run typecheck`: exit 0. `pnpm --filter @objectstack/spec run test`: 756 files, 22510 passed, 1 todo. - Generated outputs: `check:docs`, `check:authorable-surface` and `check:api-surface` exit 0, and the tree is clean after the build. `git grep "Landing, as of 2026-10-29"` on base hits only the source file, so no generated file carries this JSDoc. - Changeset: none. JSDoc and ADR prose only; the label is `skip-changeset`, as on the first pass (#27965). Out of scope, as #27956 says: the action-invocation audit row (#27908) and the platform `ai` declarations (#27900). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema change.) - [x] 2. Every name I added uses the casing for its kind. (No names added.) - [ ] 3. No formatter ran; `pnpm lint` was not run locally (no code changed; CI's ESLint job judges it). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. (Two cards that edit the same ADR-0066 D4 note, by the seat's dispatch.) - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Publishes nothing: `skip-changeset`.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (None changes; `check:docs` exit 0 on a clean tree.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (Except the Chinese maintainer brief.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4cw9gAhrah1Mtf9tSZ7 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a648ac commit 75b0c8b

2 files changed

Lines changed: 3 additions & 6 deletions

File tree

‎docs/adr/0066-unified-authorization-model.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0066: Unified authorization model — capability registry, secure-by-default posture, resource→capability contracts, dual-surface gates
22

3-
**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/<object>/<action>` 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.
3+
**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/<object>/<action>` 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.
44
**Deciders**: ObjectStack Protocol Architects
55
**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)
66
**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
7474
**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/<object>/<action>` 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/<object>/<action>`, `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.
7575

7676
> **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/<object>/<action>` — 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.
77+
>
78+
> **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/<object>/<action>` 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.
7779
7880
### D5 — Package-seeded, admin-maintainable policies [existing mechanism, fill the gap]
7981
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.

‎packages/spec/src/ui/action.zod.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,11 +1610,6 @@ const actionObject = () => strictObject({
16101610
* and ungoverned — the door does not dispatch it, MCP does not list it, the
16111611
* activation switch refuses to flip it, and nothing checks this declaration
16121612
* server-side.
1613-
*
1614-
* Landing, as of 2026-10-29: ADR-0066 records the decision; the server half,
1615-
* the ruling's step 2 (#27506), is on `main` (#27649). Until step 3 (#27611)
1616-
* lands the console fetches `target` directly; card #27694 removes this
1617-
* paragraph when it does.
16181613
*/
16191614
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.'),
16201615

0 commit comments

Comments
 (0)