Repository navigation
Commit e12d3f3
fix(runtime)!: the flow trigger door refuses a params that is not a JSON object (#25974)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XQd91J7V4CWRKjvyX2EBFVK)_
Fixes #25821
Clause-②: no (narrowing)
Metadata-protocol: none
Contract review owed: the `(narrowing)` arm. The trigger doors refuse
request shapes they used to dispatch, a `params` that is a string,
number, boolean, array or `null`. Nothing is widened, and no
`packages/spec/**` path is touched. The seat's claim (6409382263)
carries `Clause-②: no`; the line above is the measured value on this
diff, declared as the dispatch order asked.
## What changed
`buildAutomationContext` (`packages/runtime/src/domains/automation.ts`)
read `params` with `params && typeof params === 'object' ? { ...params }
: {}`. A string became an empty bag and the flow ran with no inputs. An
array was spread into an index-keyed bag. A falsy value (`null`, `0`,
`false`, `""`) took the flat-body branch, which forwarded the body's
other top-level keys as params instead.
The builder now refuses a `params` that is not a JSON object, before
anything is dispatched: `400 VALIDATION_FAILED`, one `fields[]` entry `{
field: 'params', code: 'invalid_type' }`, message `Invalid trigger body
— params expected an object (a map of names to values), received a
string` (the received type varies). It is thrown as the validation
failure both dispatcher exits and the endpoint executor already answer
as 400.
The check is one function, `objectValueFailure(field, value)`, hoisted
out of the resume door's #9416 value-shape loop. The resume door now
calls it for `inputs`, `variables` and `output` and the trigger door
calls it for `params`, so the two doors of one engine ask the same
question in the same words. The resume answers are byte-identical (its
suites pass unchanged). The resume arm's local `jsonTypeOf` stays local,
as its note asks; only this one refusal is shared.
`undefined` counts as absent, as on the resume door, so a body with no
`params` key still takes the flat-body branch. An explicit `null` is
refused, as the resume door refuses `{"inputs":null}`.
## Doors measured
| Door | Shares the builder? | Now |
|---|---|---|
| `POST /api/v1/automation/:name/trigger` | yes, through
`respondToFlowTrigger` | refuses |
| `POST /api/v1/automation/trigger/:name` (legacy,
`client.automation.trigger()`) | yes, same function | refuses |
| A declared `type: 'flow'` API endpoint (`endpoint-executor.ts`
`executeFlow`) | yes | refuses |
| `POST /api/v1/actions/:object/:action` on a `type: 'flow'` action |
no, it reads `reqBody.params` itself (`domains/actions.ts`) | unchanged,
see Acceptance notes |
| Flow-to-flow (`subflow`, `map` nodes) | no, they call `engine.execute`
with a built context | not a wire door |
| MCP | no MCP tool starts a flow by name; `verify` calls `execute` in
process | not applicable |
Order on the REST doors and the endpoint: an unknown flow still answers
`404` and a caller the flow refuses still answers `403`, before the body
is judged. The refusal is before `execute()`, so no run starts and no
run row is written.
## Evidence
Live, fresh showcase boot (`pnpm dev -- --fresh`) on this branch at
`7f0c45b4`, signed in as the seeded admin:
- `POST /api/v1/automation/showcase_notify_owner/trigger` and `POST
/api/v1/automation/trigger/showcase_notify_owner`, each with
`{"params":"oops"}`, `{"params":[1,2]}`,
`{"params":null,"ownerId":"u1"}` and `{"params":42}`: all eight answer
`400 VALIDATION_FAILED` naming `params` with the received type.
- Control `{"params":{}}` on both routes still dispatches (`400
FLOW_FAILED`, "Unknown variable: ownerId", the flow's own answer with no
inputs).
- `GET /api/v1/automation/showcase_notify_owner/runs` afterwards lists 2
runs, the two controls. The eight refused requests left no run row.
- `POST /api/v1/apps/showcase/inquiries/purge` (a declared `type:
'flow'` endpoint) with `{"params":"oops"}`: `400 VALIDATION_FAILED`
naming `params`.
Tests
(`packages/runtime/src/domains/automation-trigger-params-shape.test.ts`,
new, 42 cases):
- Red first: with `automation.ts` restored from base `9ca36b16`, 29
failed / 13 passed (every refusal case red, every preservation case
green).
- Green on the fix: 42/42, with the neighbouring suites (resume
value-shape, resume envelope, trigger route status, endpoint executor,
caller param keys, run locale, anonymous gate): 8 files, 215 tests
passed.
- Ablation (`scripts/ablation-replace.mjs`, `if (paramsFailure) {` to
`if (paramsFailure && false) {`, anchor 1 to 0, blob moved, restored to
the HEAD blob with `git diff HEAD` empty): 29 failed / 13 passed.
- `pnpm --filter @objectstack/runtime typecheck && pnpm --filter
@objectstack/runtime test` at `7f0c45b4`: lock VERDICT command-exit 0,
448 test files passed, 7224 tests passed, 19 skipped.
Gates at `7f0c45b4`: `node scripts/pm/dispatch-gates.mjs --commands`
derived 69 families; all 69 exit 0 (three needed the full build or an
unshallow fetch first and were rerun after it). `--ran` reconciliation:
69 derived, 69 run, 0 NOT-MEASURED. `eslint --no-inline-config
packages/runtime`: exit 0. Full `pnpm turbo run build`: 121/121 tasks.
Dogfood battery (the `(narrowing)` arm), full, at `7f0c45b4`: `pnpm
--filter @objectstack/dogfood test` under the lock, VERDICT command-exit
0, Test Files 296 passed / 3 skipped (299), Tests 2523 passed / 18
skipped (2541), no failures.
`origin/main` moved after the merge (10 commits, none touching
`domains/automation.ts`, `endpoint-executor.ts` or `http-dispatcher.ts`;
`git merge-tree` clean), so this head is not re-merged; CI runs on the
merge ref.
## Acceptance notes
Two sibling doors drop a non-object value the same way. Neither shares
the builder, so neither is touched here; both are reported to the seat
with their live readings.
- `POST /api/v1/actions/:object/:action` reads `params` with the same
`params && typeof params === 'object'` guard (`domains/actions.ts`). On
the fresh boot, `POST
/api/v1/actions/showcase_task/showcase_bulk_reassign` with
`{"params":"oops"}` and with `{"params":[1,2]}` each answered `200` and
started a paused run, exactly as `{"params":{}}` did.
- The trigger door's body itself is not judged: `ctxBody = body &&
typeof body === 'object' ? body : {}`. A JSON string body `"oops"`
dispatched `showcase_notify_owner` with an empty bag (`400 FLOW_FAILED`,
"Unknown variable: ownerId"), and an array body `[1,2]` forwarded its
indices as params. The resume door refuses a non-object body; the
trigger ruling covers `params` only.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ny9nEFwaZEjuu6xpu6Y4gC)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d1663ba commit e12d3f3
3 files changed
Lines changed: 284 additions & 13 deletions
File tree
- .changeset
- packages/runtime/src/domains
| 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
82 | 114 | | |
83 | 115 | | |
84 | 116 | | |
| |||
114 | 146 | | |
115 | 147 | | |
116 | 148 | | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
117 | 155 | | |
118 | 156 | | |
119 | 157 | | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
120 | 174 | | |
121 | 175 | | |
122 | 176 | | |
| |||
125 | 179 | | |
126 | 180 | | |
127 | 181 | | |
128 | | - | |
129 | | - | |
130 | | - | |
| 182 | + | |
131 | 183 | | |
132 | 184 | | |
133 | 185 | | |
134 | | - | |
| 186 | + | |
135 | 187 | | |
136 | 188 | | |
137 | 189 | | |
| |||
3352 | 3404 | | |
3353 | 3405 | | |
3354 | 3406 | | |
| 3407 | + | |
| 3408 | + | |
| 3409 | + | |
3355 | 3410 | | |
3356 | 3411 | | |
3357 | | - | |
3358 | | - | |
3359 | | - | |
3360 | | - | |
3361 | | - | |
3362 | | - | |
3363 | | - | |
3364 | | - | |
3365 | | - | |
| 3412 | + | |
| 3413 | + | |
3366 | 3414 | | |
3367 | 3415 | | |
3368 | 3416 | | |
| |||
0 commit comments