Repository navigation
Commit 86c9d53
docs(adr): ADR-0066 D4 api clause — same-origin
Fixes #27610
Clause-②: no
Metadata-protocol: semantic
Skills-docs: follow-up #27506 — `content/docs/ui/actions.mdx` (the type
table, the "api → 400, call the endpoint directly" rows),
`content/docs/ai/connect-mcp.mdx` (api never listed by `list_actions`)
and `content/docs/ai/actions-as-tools.mdx` describe today's door
truthfully and are rewritten when the server half lands; no `skills/**`
file teaches the superseded clause (grep: zero hits), and the three
generated reference pages are regenerated here.
## 维护者速读
- 改了什么:ADR-0066 D4 的「api 型动作由浏览器直连、不经过平台动作路由」条款按裁决(#27506 评论
6499781492)改为「同源 `type: 'api'` 动作经平台动作门(`POST /actions/OBJECT/ACTION` 与
MCP `run_action`)分发,门上跑 D4 门禁后转发,端点自身复核保留为纵深防御;非同源 target
维持仅浏览器、不受治理,激活开关拒绝切换」;2026-07 原条款原文保留并标为已被取代。ADR-0126 §8 第 2 项补一句:动作的
consult point 就是 `/actions` 这扇门,api 型同样经它。`action.zod.ts` 的
`requiredPermissions` 注释与 `.describe()` 同步改写(无 schema 形状变化),三份生成的参考文档随
`gen:docs` 重生成。
- 为什么改:维护者裁决 「我们的平台设计目标是 agent 和人类使用同一套系统」「同意」;今天 70 余个 api
型动作(封禁、模拟登录、改密)只有人类点击能到,agent 调不到,ADR-0126 开关对它不生效。Prime Directive
#13:已记录的决定由 ADR 修订推翻,不靠 changeset 偷改。本 PR 是三步裁决的第 1 步,只改文字。
- 风险与回滚:零运行时代码、零 schema 变化;风险只在文字先于实现——ADR 与 docblock 均注明「服务端半边由 #27506
落地,落地前门仍拒绝 `type: 'api'`」。回滚 = revert 本 PR。
- 先看:① `docs/adr/0066-unified-authorization-model.md` D4 段与其下的 Amended
注(第 74、76 行);② `packages/spec/src/ui/action.zod.ts` 的
`requiredPermissions` docblock 与 `.describe()`(第 1593–1620 行);③
`docs/adr/0126-packaged-metadata-customization-model.md` §8 第 2 项末句。
- 两分钟验证:`pnpm --filter @objectstack/spec build && pnpm --filter
@objectstack/spec check:generated` — 预期 「All 14 generated artifacts are
up to date」;`pnpm check:adr-anchors` 预期退出 0。
- 需要你做的:审阅并合并本 PR(受管面 `docs/adr/**` + `packages/spec/src/**`,Tier
H,由维护者落地)。
## What changes
Step 1 of the three-step ruling on #27506 (comment 6499781492). ADR and
spec-text amendment only: no runtime code, no schema shape change.
1. **`docs/adr/0066-unified-authorization-model.md`, D4 "Scope of the
server half".** The live paragraph now reads: the platform action door —
`POST /api/v1/actions/OBJECT/ACTION` and the MCP `run_action` bridge,
one gate (`actionPermissionError`) — dispatches `script`, `flow`, the
declarative `operation: 'update'` write, and a `type: 'api'` action
whose `target` is a same-origin platform path. For that api action the
door runs the anonymous baseline, the D4 gate and the ADR-0126 §8
consult, then forwards in-process to the declared `target` as the caller
(request assembled from `method` / `recordIdParam` / `bodyShape` /
`bodyExtra` / the placeholders in `target`; cookies and auth headers
carried; `Set-Cookie` returned). The endpoint keeps re-checking its own
authorization: defence in depth, not a replacement. An off-origin api
action stays browser-only and ungoverned: the door does not dispatch it,
MCP does not list it, and the activation write door
(`handleActionActivationWrite`) refuses to flip it with a reason. A
block-quoted **Amended (2026-10-29)** note keeps the 2026-07 clause
(framework#3923) verbatim as superseded, carries the ruling reference
and the maintainer's words untranslated, records the three landing
steps, and states today's behaviour (`isHeadlessInvokableAction` admits
`script`, `flow` and the declarative update only). The Status line gains
the matching `· **Amended** (2026-10-29, …)` entry in the file's
existing style. The paragraph also drops `modal` from the dispatched
types: on `origin/main` the door answers a `modal` action 400 as a
client-side type (`headlessActionTypeError`), so the 2026-07 list was
already inaccurate.
2. **`docs/adr/0126-packaged-metadata-customization-model.md` §8 item
2.** One sentence: the action consult point is the `/actions` door
(`disabledActionRefusal`, both doors) and covers `type: 'api'` through
it per ADR-0066 D4 as amended, with the off-origin exception — per §4
"each consumer documents its consult point".
3. **`packages/spec/src/ui/action.zod.ts`, `requiredPermissions`.**
Docblock and `.describe()` reworded to the amended rule, with a landing
note in the docblock. No schema shape change. The old "fetched by the
browser / never passes the route / endpoint must re-check" wording and
the `script/flow/modal` list are gone from the spec.
4. **Generated** by `pnpm --filter @objectstack/spec check:generated
--fix` (only `check:docs` was stale):
`content/docs/references/ui/action.mdx`,
`content/docs/references/data/object.mdx`,
`content/docs/references/kernel/metadata-plugin.mdx` — one table row
each. Never hand-edited.
5. **Changeset** `.changeset/27610-adr-0066-d4-api-actions-door.md`:
`@objectstack/spec` **`minor`**, `Clause-②: no`, `Metadata-protocol:
semantic`. The card and the claim said `patch`; ruling A
(`check-changeset-no-major.mjs`, the level axis) grades a declared kind
on a moved package `minor` and refuses `patch` as `enforce`, and
AGENTS.md Post-Task Checklist step 3 says the same, so the level follows
the kind. Flagged in the report as a dispatch-vs-standard-clause
conflict, resolved in the standard clause's favour.
**Every other sentence on `main` that restates the old api clause**,
found by grep (`fetched by the browser`, `called by the browser`,
`self-authored endpoint`, `pointed at a custom endpoint`, `never passes
through that route`, `platform never sees the request`) and classified:
- Restates it and is in this card's surface → changed: `docs/adr/0066`
L74 (and the Status line L3), `packages/spec/src/ui/action.zod.ts`
docblock + `.describe()`, the three generated reference pages.
- Mentions the gate but not the browser clause → unchanged, still true:
`action.zod.ts` the alias comment near `permissions:
'requiredPermissions'` and the `hidden` guidance ("enforced with a 403
on the platform action route").
- About the api action's response or undo, not the gate → unchanged:
`action.zod.ts` `onSuccess`/`outcomeMessages` ("the `target` call's
response body") and `undoable` ("`type: 'api'` is fulfilled by the
console").
- Describes today's door truthfully, becomes false when the server half
lands → not edited here (it would advertise a capability the runtime
does not deliver yet), named on the `Skills-docs:` line for #27506:
`content/docs/ui/actions.mdx` (type table; "api → 400 — it dispatches on
`target`; call that endpoint directly"; the 400 troubleshooting row),
`content/docs/ai/connect-mcp.mdx` ("`url`, `modal`, `form`, and `api`
have no headless …"; `list_actions` row),
`content/docs/ai/actions-as-tools.mdx` (type table row).
- Runtime comments and prescriptions, #27506's surface → unchanged:
`packages/runtime/src/action-execution.ts` `headlessActionTypeError`
docblock and its api prescription ("Call its `target` endpoint
directly"), `packages/runtime/src/domains/actions.ts` the type-dispatch
comment ("a `url`/`modal`/`form`/`api` action reaching this endpoint has
nowhere to go").
- UI lane (step 3's surface) → unchanged, listed for the ui card:
`ui/packages/components/src/renderers/layout/containers.tsx`
(capability-gate comment: "never for a `type: 'api'` action pointed at a
custom endpoint"),
`ui/packages/components/src/__tests__/action-bar.test.tsx`,
`ui/packages/components/src/__tests__/page-header-capability-gate.test.tsx`,
`ui/packages/plugin-grid/src/__tests__/bulkActionCapabilityGate.test.tsx`
(the same sentence in their docblocks).
## Tests
No behaviour changes, so no new tests. The affected package's suites
were run under the shared verify lock on head `e44fd034`:
<details><summary>`pnpm --filter @objectstack/spec typecheck` — lock
VERDICT command-exit 0 (79s)</summary>
```
os-verify-lock: VERDICT command-exit 0 · held the lock 79s (1m19s) · waited 1s
```
</details>
<details><summary>`pnpm --filter @objectstack/spec test` — lock VERDICT
command-exit 0 (204s): Test Files 752 passed (752) · Tests 22343 passed
| 1 todo (22344)</summary>
```
Test Files 752 passed (752)
Tests 22343 passed | 1 todo (22344)
os-verify-lock: VERDICT command-exit 0 · held the lock 204s (3m24s) · waited 0s
```
</details>
Ablation: none — the diff changes no tested behaviour (prose, a
docblock, a `.describe()` and its generated projection).
## Acceptance notes
- **Changeset level.** `minor`, not the `patch` the card named — see
"What changes" item 5. If the maintainer wants `patch`, the kind would
have to be `none`, which the triage and the claim both ruled out
(`semantic`).
- **`modal` dropped from the D4 list.** Factual correction inside the
amended paragraph, verified against `origin/main` `0e33108a`:
`SERVER_DISPATCHED_ACTION_TYPES` is `script`, `flow`;
`headlessActionTypeError` returns the client-side prescription for
`modal` and the route answers it 400 (`domains/actions.ts`, the
type-dispatch branch). Listed as a changed sentence in the report.
- **Today vs after #27506.** Every amended text names the landing: the
ADR note states the three steps and today's refusal; the spec docblock
carries a "Landing" paragraph; the `.describe()` cites "ADR-0066 D4 as
amended 2026-10-29" (no tracker id in a published string). The runtime
on `main` is unchanged by this PR: `isHeadlessInvokableAction` admits
`script`, `flow` and the declarative update only;
`handleActionActivationWrite` does not yet refuse an api action; the
console still fetches `target`.
- **Not filed as cards (observations only, class-less):** the `ui/`
docblocks above restate the superseded clause and are the ui card's to
rewrite; `content/docs/ui/actions.mdx` row "`api` | **400**" and
`connect-mcp.mdx`'s `list_actions` row are #27506's docs to flip.
- **No `scripts/adr-anchors` change**: `pnpm check:adr-anchors` exit 0;
no anchor names `docs/adr/0066*`, `docs/adr/0126*` or `action.zod.ts`.
## Verification
Head `e44fd034` (branch
`claude/issue-27610-adr-0066-d4-api-actions-door`, base `origin/main` at
merge base `0e33108a`). Diff: 7 files, +49/−16.
<details><summary>Build + generated (lock VERDICT 0; `check:generated` 1
of 14 stale → `--fix` → 14/14 up to date)</summary>
```
pnpm --filter @objectstack/spec build → os-verify-lock: VERDICT command-exit 0 · held the lock 87s
pnpm --filter @objectstack/spec check:generated → exit 1: ✗ 1 of 14 artifact(s) stale: check:docs (content/docs/references/**)
pnpm --filter @objectstack/spec check:generated --fix → exit 0: ✓ gen:docs · re-check ✓ check:docs
pnpm --filter @objectstack/spec check:generated → exit 0: ✓ All 14 generated artifacts are up to date
```
</details>
<details><summary>Derived gates (`node scripts/pm/dispatch-gates.mjs
--commands`, 101 families on the final diff): 94 exit 0, 7 exit 3
(prerequisite not met — NOT MEASURED locally, CI measures); `--ran`
reconciliation: 101 derived · 94 run · 7 NOT-MEASURED · 0 UNRUN, exit
0</summary>
```
exit 0 (94): check:adr-anchors · check:adr-index · check:adr-symbol-anchors · check:adr-links (+self-test) · check:doc-authoring ·
check-doc-route-spelling --advisory (+self-test) · check-docs-section-name (+self-test) · check-section-landing-index (+self-test) ·
spec: check:generated (14/14 up to date) · check:corpus-claim-drift · check:doc-anchors · check:doc-frontmatter · check:docs-audit-scope ·
check:docs-redirects · check:docs-single-h1 · check:docs-spec-enumerations · check:published-readme-links ·
check:react-page-adapter-contract · check:role-word · check:vendor-version-stamps ·
check:nul-bytes · check:issue-citations · check:closed-ceremony-text · check:pm-prior-rulings · check:pm-governed-merges ·
check-adr-0087-registration --base origin/main (+self-test) · check-empty-changeset --base origin/main (+self-test) ·
check-changeset-no-major --base origin/main · check:changeset-gate-self-tests · check:pm-changeset-deadline-census ·
check-closing-keyword-parity (+self-test) · spec: check:api-surface, check:authorable-surface, check:browser-reachable-entries,
check:docs, check:dual-source-exports, check:duration-unit-keys, check:empty-state, check:entry-nameability, check:export-origins,
check:exported-any, check:liveness, check:llms-txt, check:skill-refs, check:strictness-ledger, check:variant-docs, check:yaml-examples ·
check:spec-docblock-symbol-anchors · check:spec-parsed-alias · check:published-files · check:dts-closure ·
check:sourcemap-no-sources-content · check-dts-emitted --self-test · check-dev-prereqs --self-test · the comment-mask, plugin-teardown
(run), ci/*, docs-audit/*, release-rehearsal-clone, release-pending-publish self-tests · check:ci-filter-parity ·
check:cross-package-test-inputs · check:dispatcher-error-vocabulary · check:driver-memory-census · check:gitlink-declared ·
check:keyed-text-bounds · check:logger-receiver-detach · check:merge-driver · check:org-identifier · check:page-declaration-shape ·
check:platform-object-tenancy-census · check:refd-timer-probe · check:registry-log-declared · check:rest-log-spy-declared ·
check:skill-identifier-liveness · check:slot-lookup · check:system-context-census · check:test-source-alias ·
check:tier-file-adoption · check:undeclared-dep-imports · check:watch-hint-literal
exit 3 (7, PREREQUISITE NOT MET in a fresh worktree where only packages/spec is built — declared narrowing: the
closure these read is 39 workspace packages, a repo-level build that is CI's run, and none reads the prose this diff moves):
pnpm --filter @objectstack/lint run check:doc-formula-expressions → `@objectstack/formula` not built
pnpm --filter @objectstack/lint run check:doc-security-posture → `@objectstack/lint` not built
pnpm --filter @objectstack/spec run check:skill-examples → packages/client-react/dist holds no declarations
pnpm check:docs-transcript-drift → packages/lint/dist absent (reads the built registry)
pnpm check:dual-build-cjs-loads → 90 packages' dist absent; "Run pnpm build first"
pnpm check:lean-entry-closure → packages/objectql/dist absent
node scripts/check-plugin-teardown-shape.mjs --self-test → fixture pinned to a commit outside the shallow clone (the gate's own run exits 0)
node scripts/pm/dispatch-gates.mjs --ran ran.list → exit 0: "101 derived famil(ies) accounted for — 94 run, 7 NOT-MEASURED (7 DERIVED from a recorded exit 3)"
```
</details>
<details><summary>Local-only families (readings, not gates)</summary>
```
pnpm check:skill-frame-freshness → exit 0 (frame current with origin/main)
pnpm check:dev-prereqs → exit 1 (104 of 105 workspace packages have no dist in this fresh worktree; a machine-state reading, "Fix: pnpm build")
```
</details>
Dogfood battery: n/a — no `!` in the title and no `(narrowing)` arm.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ihL2dPRZXfgE1A2Vct3FU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>type: 'api' actions are dispatched by the platform action door (#27664)1 parent c1ae3d1 commit 86c9d53
13 files changed
Lines changed: 64 additions & 22 deletions
File tree
- .changeset
- content/docs/references
- data
- kernel
- ui
- docs/adr
- packages
- platform-objects/src/apps/translations
- spec/src/ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
502 | 502 | | |
503 | 503 | | |
504 | 504 | | |
505 | | - | |
| 505 | + | |
506 | 506 | | |
507 | 507 | | |
508 | 508 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
347 | 347 | | |
348 | 348 | | |
349 | 349 | | |
350 | | - | |
| 350 | + | |
351 | 351 | | |
352 | 352 | | |
353 | 353 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
| 60 | + | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| |||
0 commit comments