Repository navigation
fix(runtime)!: the action and flow trigger doors refuse a body or params that is not a JSON object, through the resume door's shape check - #26034
Conversation
…object input (red) Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An Co-authored-by: Claude <noreply@anthropic.com>
…he resume body refuse non-object input through one shape check Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An Co-authored-by: Claude <noreply@anthropic.com>
…on-failure.ts, not among the dispatcher domains Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An Co-authored-by: Claude <noreply@anthropic.com>
…ame shape check Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5d872fbfe2b3f11ccaac09f9a536484ca2f4fd69 && git checkout 5d872fbfe2b3f11ccaac09f9a536484ca2f4fd69
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 39ca58787acbadd3b6e28810e2baa8576bd0d243 e321fc54f209cbba4d1ea74fcb8639ea8e027a42 && git checkout -B drift-repro 39ca58787acbadd3b6e28810e2baa8576bd0d243 && git merge --no-ff e321fc54f209cbba4d1ea74fcb8639ea8e027a42
node scripts/docs-audit/affected-docs.mjs --json 39ca58787acbadd3b6e28810e2baa8576bd0d243
|
Contract reviewServed-tier: Inputs: card #25976 body and its three comments (triage 6411002575, claim 6411222289, os-dev-report 6413120939); the sibling card's contract review 6410594343 and PR #25974 (body, file list; its merged code is what ① Derived judgmentsCI on the head at write time. 46 check-runs: 29 a. The action door: which inputs are now refused, with what answer — right. b. The trigger doors' body — right, and reaches exactly the wire doors. c. The resume door: no observable change beyond the declared one — right. d. No error code is minted. The top-level code is e. Public surface: unchanged. f. Tests. The new suite drives the real ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | draft, base main, Fixes #25976, Clause-②: no (narrowing), Metadata-protocol: none, patch with the BREAKING banner; every path under packages/runtime/** plus one changeset; no packages/spec/** path. |
| Narrowing | every run door's object-shaped input goes through the one objectValueFailure (moved from domains/automation.ts to packages/runtime/src/object-value-shape.ts): the action door's body and params, the trigger builder's body (both REST trigger routes and every type: flow endpoint), and the resume door's body. A non-object value, coerced and run on main, is refused 400 VALIDATION_FAILED with one fields[] row (invalid_type) before dispatch, writing no run row. No body, a JSON null body and an absent params keep the empty bag. |
| Departures, both accepted by the review | (1) the action door's whole BODY was coerced the same way on the live probe and is fixed in place (same class, same door, a file the claim holds); (2) the shared module sits outside src/domains/ because check:route-envelope requires every file there to be a declared dispatcher domain. |
| Evidence | red first (78 failed at the base), three ablations red; runtime 453 files / 7399 passed; 69/69 derived gates exit 0. Auto-merge holds for the head's Test Core shards still running at review time. |
| Carried | the dogfood authz-probe-blind-spot red was main's own, fixed on main by #26002. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready + auto-merge through the relay. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #25976
Clause-②: no (narrowing)
Metadata-protocol: none
Contract review owed: the
(narrowing)arm. The action door and the flow trigger doors refuse request shapes they used to dispatch: a body that is a string, number, boolean or array, and (action door) aparamsthat is a string, number, boolean, array ornull. Nothing is widened, and nopackages/spec/**path is touched. The seat's claim (6411222289) carriesClause-②: no; the line above is the measured value on this diff, declared as the dispatch order asked.What changed
PR #25974 hoisted one value-shape check,
objectValueFailure(field, value), out of the resume door and made the trigger door ask it ofparams. Two neighbouring reads still coerced silently. This PR routes every run door's object-shaped input through that one function:POST /api/v1/actions/:object/:action,paramsparams && typeof params === 'object' ? params : {}: a string became an empty bag, an array was handed on as the bag400 VALIDATION_FAILED,{ field: 'params', code: 'invalid_type' }body && typeof body === 'object' ? body : {}: a JSON string or array body ran with an empty bag400 VALIDATION_FAILED,{ field: '(body)', code: 'invalid_type' }buildAutomationContext(both REST trigger routes and every declaredtype: 'flow'endpoint), the body400 VALIDATION_FAILED,{ field: '(body)', code: 'invalid_type' }POST /api/v1/automation/:name/runs/:runId/resume, the bodytypeof … !== 'object' || Array.isArray(…)check, a second spelling of the same questionobjectValueFailure; its refusal sentence is byte-identical, and itsfields[]message gains thereceived …suffix the other doors carryThe check moved from
domains/automation.tstopackages/runtime/src/object-value-shape.ts, besidevalidation-failure.ts. It gained one optional argument,expected, which only changes the wording: the resume body names its closed key set (an object with inputs, variables, output, branchLabel), every other caller getsan object (a map of names to values). The judgement is one function, with no copy.Absence keeps today's empty bag everywhere: no body, a JSON
nullbody, and a body with noparamskey all dispatch as before.undefinedcounts as absent and an explicitparams: nullis refused, exactly as PR #25974 rules it for the trigger door'sparamsand as the resume door rules{"inputs":null}.Order is unchanged around the new refusal. On the action door it sits after the declaration lookup, the permission gate, the activation consult, the type check and the automation-service probe, and before the param contract, the record load and dispatch, so a caller who may not run the action still gets
403/404first and a refused call reads no row. On the trigger doors it is where PR #25974 put theparamsrefusal: after404and403, beforeexecute().Departure from the claim's wording, declared
The claim names the action door's
params. The live probe on this branch found the action door's body read with the same truthy-object guard:"oops"and[1,2]as the whole body each answered200and started a run. It is the same defect class on the same door, in a file this claim already holds, closed with the same function and the same gate families, so it is fixed in place here (bounded in-place fix) rather than filed. The changeset and the tests cover it.Evidence
Live, fresh showcase boot (
pnpm dev -- --fresh), signed in as the seeded admin, on this branch:POST /api/v1/actions/showcase_task/showcase_bulk_reassignwith{"params":"oops"},{"params":[1,2]},{"params":null},{"params":42}: each400 VALIDATION_FAILED,fields: [{ field: 'params', code: 'invalid_type', message: 'expected an object (a map of names to values), received a string' }](the received type varies). With the body"oops",[1,2],42(heade321fc54): each400 VALIDATION_FAILEDat(body). Controls{"params":{}},{}andnulleach200with a paused run.GET /api/v1/automation/showcase_reassign_wizard/runsafterwards listed exactly the control runs (2 on the first boot, 3 on the second): the refused calls left no run row.POST /api/v1/automation/showcase_notify_owner/triggerand the legacyPOST /api/v1/automation/trigger/showcase_notify_owner, each with the body"oops",[1,2],42,true: all eight400 VALIDATION_FAILEDat(body);{"params":"oops"}still400namingparams. Controlsnull,{}and{"params":{}}each dispatch (400 FLOW_FAILED"Unknown variable: ownerId", the flow's own answer with no inputs).GET …/showcase_notify_owner/runsafterwards: 6 runs, the six controls.POST /api/v1/automation/showcase_reassign_wizard/runs/RUN_ID/resumewith"oops",[1,2],42:400 VALIDATION_FAILED, messageInvalid resume body — expected an object with inputs, variables, output, branchLabel, received a string(backticked key names; byte-identical tomain),fields[0].messagenow carrying the received type.Tests (
packages/runtime/src/domains/run-door-input-shape.test.ts, new; four rows added tofield-error-code-catalog.test.ts's census):domains/automation.tsanddomains/actions.tsrestored from base05f7758f, the two files ran 78 failed / 59 passed (every refusal case red, every preservation case green).scripts/ablation-replace.mjs, each anchor 1 to 0, each restored to the HEAD blob withgit diff HEADempty), the same two files: action body check toundefined: 17 failed; actionparamscheck toundefined: 36 failed; trigger body check toundefined: 33 failed.e321fc54:pnpm --filter @objectstack/runtime typecheck && pnpm --filter @objectstack/runtime testunder the lock, VERDICT command-exit 0, 453 test files passed, 7399 tests passed, 19 skipped. The neighbouring suites (trigger params shape, resume value shape and envelope, trigger route status, caller param keys, record-load-denied action door, flow-start permissions, declarative update, flow input params) are inside that run and were also run alone before the merge: 11 files, 356/356.Gates at
e321fc54:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 69 families against merge basec8cb510a; all 69 exit 0 (two needed the full build or an unshallow fetch and were green on this rerun).--ranreconciliation: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.eslint --no-inline-config packages/runtime: exit 0. Fullpnpm turbo run buildafter theorigin/mainmerge: 121/121 tasks.Dogfood battery (the
(narrowing)arm), full, ate321fc54:pnpm --filter @objectstack/dogfood testunder the lock, VERDICT command-exit 1, Test Files 1 failed / 295 passed / 3 skipped (299), Tests 1 failed / 2522 passed / 18 skipped (2541). The one failure is not this diff's:test/authz-probe-blind-spot.test.ts>packages/runtime/src/domains/i18n.ts — every positive control is still present in THAT file, controlhandleI18nRequestexpected 3, received 4.domains/i18n.tsand the census file are byte-identical toorigin/mainon this branch (git diff origin/main HEADempty for both), andorigin/main'si18n.tscarries 4 occurrences since69fa23d6(the ETag change), so this is a red onmain's full battery, reported to the seat rather than fixed here.origin/mainwas merged once before opening (c8cb510a; none of its commits touchdomains/actions.ts,domains/automation.ts,endpoint-executor.tsorhttp-dispatcher.ts).Acceptance notes
run_actiondoor readsparamswith the same truthy-object guard (action-execution.ts,invokeBusinessAction), but it is not reachable with a non-object value: measured on the same boot,tools/call run_actionwithparams: "oops"andparams: [1,2]is refused by the tool's own input schema (-32602, "expected record, received string at params"). Not touched.readActivationBody, the toggle, run-lifecycle and clone bodies) already refuse a non-object body and are not run doors of this card; they keep their own wording.packages/runtime/src/rather thansrc/domains/: every file undersrc/domains/must be a declared dispatcher domain (check:route-envelope), and this one answers nothing. That is one path outside the claim's declared file surface (packages/runtime/src/domains/**).Generated by Claude Code