Skip to content

Commit 4ca7af7

Browse files
committed
docs(adr): ADR-0104 D2 scopes the param-less pass-through to actions with a handler
A dated note under D2 item 1, a status-line entry, and a one-line pointer at the 2026-07-30 addendum's restatement: a declarative operation 'update' action's empty or absent params list is an empty accept set, refused through D2's own path, and at the MCP run_action door a D2 refusal carries code + status, as at the REST door. Measured on origin/main ee882b8, after merge commit d79dc48. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
1 parent ee882b8 commit 4ca7af7

1 file changed

Lines changed: 41 additions & 1 deletion

File tree

‎docs/adr/0104-field-runtime-value-shape-contract.md‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
physical column — a string column holding the bare `sys_file` id, switched
1414
per deployment on the `adr-0104-file-references` flag, never per version;
1515
driver card #15989 implements it.
16+
· **Amended** (2026-10-19, #24268, PR #24392, merge commit `d79dc4883c`) —
17+
D2's param-less pass-through is scoped to actions with a handler: a
18+
declarative `operation: 'update'` action's empty or absent `params` list is
19+
an empty accept set, refused through D2's own path, and at the MCP
20+
`run_action` door a D2 refusal carries `code` + `status` (the ADR-0112
21+
envelope), as at the REST door. See the note under D2 item 1.
1622
- **Date**: 2026-07-22
1723
- **Issue**: design follow-up generalizing #3405 / #3406 (inline lookup param
1824
silently stripped); relates #3407 (silently dropped writes), #1878 / #1891
@@ -194,6 +200,37 @@ free. Three changes:
194200
`400 VALIDATION_FAILED` per-field envelope *before* the handler runs.
195201
Actions declaring no `params` keep today's pass-through (there is nothing
196202
to check against), so existing param-less actions are untouched.
203+
204+
> **Amended 2026-10-19 (#24268, PR #24392, merge commit `d79dc4883c`) —
205+
> the param-less pass-through is scoped to actions with a handler.** The
206+
> sentence above was written when every action had a handler. A
207+
> declarative `operation: 'update'` action (#14092, executed by #15079) has
208+
> none: its bag is merged over `patch` and written to the row, so "there is
209+
> nothing to check against" does not hold for it. Its declared `params`
210+
> list is its whole wire write set, and an empty or absent list is an
211+
> **empty accept set**, not an absent contract.
212+
> `packages/runtime/src/action-execution.ts#enforceActionParams` judges
213+
> such a bag against `[]` with the same validator, so every key except the
214+
> dispatcher built-ins
215+
> (`packages/spec/src/ui/action-params.zod.ts#ACTION_PARAM_BUILTIN_KEYS`:
216+
> `recordId`, `objectName`, `_selectedIds`) is refused through this item's
217+
> path: the same 400 at the REST door and the same
218+
> `OS_ALLOW_LAX_ACTION_PARAMS` escape hatch, with no switch of its own. An
219+
> empty bag still writes the `patch`. The discriminator is `operation`
220+
> alone (`packages/runtime/src/action-execution.ts#isDeclarativeUpdateAction`),
221+
> so every other param-less action (a script handler, an inline `body`, a
222+
> `flow` target) keeps the pass-through. At the MCP `run_action` door,
223+
> `packages/runtime/src/action-execution.ts#invokeBusinessAction` throws
224+
> every D2 refusal with `code` + `status`, the 400 and the code the REST
225+
> door derives from it, and `run_action` renders a thrown code as the
226+
> ADR-0112 envelope `{ error: { code, message, status } }`
227+
> (`packages/mcp/src/mcp-http-tools.ts#thrownRefusalEnvelope`), as at the
228+
> REST door. Pinned in `packages/runtime/src/action-params-enforcement.test.ts`
229+
> (the gate, the hatch), `packages/runtime/src/action-declarative-update.test.ts`
230+
> (both doors, nothing written, and the handler control) and
231+
> `packages/mcp/src/mcp-coded-refusal.test.ts` (`run_action`'s rendering of
232+
> a thrown code). No other part of D2 changes.
233+
197234
2. **Typed handler surface.** `defineAction` today validates config only. It
198235
gains a typed companion so registered handlers stop being
199236
`(ctx: any) => any`: `ctx.params` is typed by mapping each declared param
@@ -939,7 +976,10 @@ explicitly-set case; the v17 release notes carry the migration section (what
939976
a rejected bag looks like, how to fix the caller, the escape hatch's name).
940977

941978
Actions declaring no `params` keep their pass-through — that boundary is
942-
unchanged from D2's landing. And the asymmetry with D1 stays load-bearing in
979+
unchanged from D2's landing. *(Amended 2026-10-19, #24268: the boundary holds
980+
for actions with a handler; a declarative `operation: 'update'` action's
981+
empty or absent `params` list is an empty accept set — see the note under D2
982+
item 1.)* And the asymmetry with D1 stays load-bearing in
943983
both directions: a version flip is wrong where complete per-deployment
944984
evidence is *possible* (D1's data), and right where it is impossible in
945985
principle and the failure mode is a self-describing 400 to the party who can

0 commit comments

Comments
 (0)