|
13 | 13 | physical column — a string column holding the bare `sys_file` id, switched |
14 | 14 | per deployment on the `adr-0104-file-references` flag, never per version; |
15 | 15 | 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. |
16 | 22 | - **Date**: 2026-07-22 |
17 | 23 | - **Issue**: design follow-up generalizing #3405 / #3406 (inline lookup param |
18 | 24 | silently stripped); relates #3407 (silently dropped writes), #1878 / #1891 |
@@ -194,6 +200,37 @@ free. Three changes: |
194 | 200 | `400 VALIDATION_FAILED` per-field envelope *before* the handler runs. |
195 | 201 | Actions declaring no `params` keep today's pass-through (there is nothing |
196 | 202 | 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 | +
|
197 | 234 | 2. **Typed handler surface.** `defineAction` today validates config only. It |
198 | 235 | gains a typed companion so registered handlers stop being |
199 | 236 | `(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 |
939 | 976 | a rejected bag looks like, how to fix the caller, the escape hatch's name). |
940 | 977 |
|
941 | 978 | 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 |
943 | 983 | both directions: a version flip is wrong where complete per-deployment |
944 | 984 | evidence is *possible* (D1's data), and right where it is impossible in |
945 | 985 | principle and the failure mode is a self-describing 400 to the party who can |
|
0 commit comments