Skip to content

Commit 86c9d53

Browse files
docs(adr): ADR-0066 D4 api clause — same-origin type: 'api' actions are dispatched by the platform action door (#27664)
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>
1 parent c1ae3d1 commit 86c9d53

13 files changed

Lines changed: 64 additions & 22 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
spec(ui): `requiredPermissions` on a `type: 'api'` action — a same-origin api action is gated at the platform action door (ADR-0066 D4 amended 2026-10-29)
7+
8+
Clause-②: no
9+
Metadata-protocol: semantic
10+
11+
The documented meaning of `requiredPermissions` on a `type: 'api'` action changes; no key, accepted value or default moves, and the schema shape is unchanged.
12+
13+
- **Before.** The `requiredPermissions` docblock and `.describe()` (and the generated references under `content/docs/references/`), and the Studio action form's `requiredPermissions` help text (with its `en` / `zh-CN` / `ja-JP` / `es-ES` bundles in `@objectstack/platform-objects`), said a `type: 'api'` action pointed at a custom endpoint is called by the browser directly, never passes through the platform action route, and that the endpoint must re-check the capability itself — the 2026-07 scope note of ADR-0066 D4.
14+
- **Now.** Per ADR-0066 D4 as amended on 2026-10-29, 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 — not `//`, nor `/` followed by a backslash) is dispatched by the platform action door — `POST /actions/:object/:action` and the MCP `run_action` bridge — which runs the D4 capability gate, the anonymous baseline and the ADR-0126 activation consult before forwarding the call in-process to the declared `target` as the caller. MCP keeps its `ai.exposed` opt-in and its fail-closed refusal of actions on `sys_*` objects, so those (`sys_user.ban_user` and the like) reach an agent through REST `POST /actions/:object/:action`, not MCP. The door gates the calls it dispatches, and only those: the target endpoint stays directly reachable, so it must still enforce the declared capability itself, the door's gate being the added check (defence in depth, not a replacement). The `.describe()` and the help text say "on every call the door dispatches", so they stay true before and after the server half lands. An api action whose `target` is not a same-origin platform path stays browser-only and ungoverned: the door does not dispatch it, MCP does not list it, and the activation switch refuses to flip it. The `operation: 'update'` refusal beside a `type: 'api'` executor now says `api` "calls the endpoint it names" rather than that the browser calls it. The description and the help text also drop `modal` from the list of server-dispatched types: the door dispatches `script`, `flow` and the declarative `operation: 'update'` write, and answers a `modal` action 400 as a client-side type.
15+
- **Landing.** ADR-0066 records the decision; the runtime that implements the door's api forwarding ships separately (ADR-0066 D4's amendment note names the server card). Until it lands the door still refuses `type: 'api'` and the console fetches `target` directly. Nothing an author writes today is refused or re-read differently by this release.

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ const result = ApiMethod.parse(data);
502502
| **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| …>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions at parse time. |
503503
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
504504
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason a greyed action shows (its hover title and accessible description, or a menu item's second line) while `disabled` evaluates TRUE — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
505-
| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. |
505+
| **requiredPermissions** | `string[]` | optional | [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. |
506506
| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record<string, object>; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
507507
| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. |
508508
| **recordIdField** | `string` | optional | Row field whose value seeds recordIdParam. Defaults to "id". |

‎content/docs/references/kernel/metadata-plugin.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ const result = MetadataBulkResultSchema.parse(data);
347347
| **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| …>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions at parse time. |
348348
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
349349
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason a greyed action shows (its hover title and accessible description, or a menu item's second line) while `disabled` evaluates TRUE — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
350-
| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. |
350+
| **requiredPermissions** | `string[]` | optional | [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. |
351351
| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record<string, object>; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
352352
| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. |
353353
| **recordIdField** | `string` | optional | Row field whose value seeds recordIdParam. Defaults to "id". |

‎content/docs/references/ui/action.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const result = ActionSchema.parse(data);
5757
| **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| 'transfer_ownership' \| 'remove_member' \| 'create_team' \| 'update_team' \| 'remove_team' \| … +4 more>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions at parse time. |
5858
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
5959
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason a greyed action shows (its hover title and accessible description, or a menu item's second line) while `disabled` evaluates TRUE — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
60-
| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. |
60+
| **requiredPermissions** | `string[]` | optional | [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. |
6161
| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record<string, object>; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
6262
| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. |
6363
| **recordIdField** | `string` | optional | Row field whose value seeds recordIdParam. Defaults to "id". |

0 commit comments

Comments
 (0)