Skip to content

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

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-jpr5ee
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-jpr5ee

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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) 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 (6411222289) carries Clause-②: 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 of params. Two neighbouring reads still coerced silently. This PR routes every run door's object-shaped input through that one function:

Door Read before Now
POST /api/v1/actions/:object/:action, params params && typeof params === 'object' ? params : {}: a string became an empty bag, an array was handed on as the bag 400 VALIDATION_FAILED, { field: 'params', code: 'invalid_type' }
the same door, the body body && typeof body === 'object' ? body : {}: a JSON string or array body ran with an empty bag 400 VALIDATION_FAILED, { field: '(body)', code: 'invalid_type' }
buildAutomationContext (both REST trigger routes and every declared type: 'flow' endpoint), the body the same truthy-object guard: a string body dispatched with an empty bag, an array body reached the flat-body branch and forwarded its indices as params 400 VALIDATION_FAILED, { field: '(body)', code: 'invalid_type' }
POST /api/v1/automation/:name/runs/:runId/resume, the body its own typeof … !== 'object' || Array.isArray(…) check, a second spelling of the same question the same refusal as before, now asked through objectValueFailure; its refusal sentence is byte-identical, and its fields[] message gains the received … suffix the other doors carry

The check moved from domains/automation.ts to packages/runtime/src/object-value-shape.ts, beside validation-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 gets an 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 null body, and a body with no params key all dispatch as before. undefined counts as absent and an explicit params: null is refused, exactly as PR #25974 rules it for the trigger door's params and 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/404 first and a refused call reads no row. On the trigger doors it is where PR #25974 put the params refusal: after 404 and 403, before execute().

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 answered 200 and 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_reassign with {"params":"oops"}, {"params":[1,2]}, {"params":null}, {"params":42}: each 400 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 (head e321fc54): each 400 VALIDATION_FAILED at (body). Controls {"params":{}}, {} and null each 200 with a paused run. GET /api/v1/automation/showcase_reassign_wizard/runs afterwards 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/trigger and the legacy POST /api/v1/automation/trigger/showcase_notify_owner, each with the body "oops", [1,2], 42, true: all eight 400 VALIDATION_FAILED at (body); {"params":"oops"} still 400 naming params. Controls null, {} and {"params":{}} each dispatch (400 FLOW_FAILED "Unknown variable: ownerId", the flow's own answer with no inputs). GET …/showcase_notify_owner/runs afterwards: 6 runs, the six controls.
  • Resume door, POST /api/v1/automation/showcase_reassign_wizard/runs/RUN_ID/resume with "oops", [1,2], 42: 400 VALIDATION_FAILED, message Invalid resume body — expected an object with inputs, variables, output, branchLabel, received a string (backticked key names; byte-identical to main), fields[0].message now carrying the received type.

Tests (packages/runtime/src/domains/run-door-input-shape.test.ts, new; four rows added to field-error-code-catalog.test.ts's census):

  • Red first: with domains/automation.ts and domains/actions.ts restored from base 05f7758f, the two files ran 78 failed / 59 passed (every refusal case red, every preservation case green).
  • Ablations (scripts/ablation-replace.mjs, each anchor 1 to 0, each restored to the HEAD blob with git diff HEAD empty), the same two files: action body check to undefined: 17 failed; action params check to undefined: 36 failed; trigger body check to undefined: 33 failed.
  • Green at e321fc54: pnpm --filter @objectstack/runtime typecheck && pnpm --filter @objectstack/runtime test under 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/objectstack derived 69 families against merge base c8cb510a; all 69 exit 0 (two needed the full build or an unshallow fetch and were green on this rerun). --ran reconciliation: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN. eslint --no-inline-config packages/runtime: exit 0. Full pnpm turbo run build after the origin/main merge: 121/121 tasks.

Dogfood battery (the (narrowing) arm), full, at e321fc54: pnpm --filter @objectstack/dogfood test under 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, control handleI18nRequest expected 3, received 4. domains/i18n.ts and the census file are byte-identical to origin/main on this branch (git diff origin/main HEAD empty for both), and origin/main's i18n.ts carries 4 occurrences since 69fa23d6 (the ETag change), so this is a red on main's full battery, reported to the seat rather than fixed here.

origin/main was merged once before opening (c8cb510a; none of its commits touch domains/actions.ts, domains/automation.ts, endpoint-executor.ts or http-dispatcher.ts).

Acceptance notes

  • The MCP run_action door reads params with 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_action with params: "oops" and params: [1,2] is refused by the tool's own input schema (-32602, "expected record, received string at params"). Not touched.
  • Other doors with their own body checks (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.
  • The new module lives in packages/runtime/src/ rather than src/domains/: every file under src/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

…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>
…on-failure.ts, not among the dispatcher domains

Claude-Session: https://claude.ai/code/session_01V8GCLpAeKZ1wyQV8dx54An
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 5 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via invalid_type (literal, a string literal in handleActionsRequest; a string literal in handleAutomationRequest; a string literal in objectValueFailure; a string literal on a changed line))
  • content/docs/api/error-handling-server.mdx (via invalid_type (literal, a string literal in handleActionsRequest; a string literal in handleAutomationRequest; a string literal in objectValueFailure; a string literal on a changed line))
  • content/docs/automation/flows.mdx (via invalid_type (literal, a string literal in handleActionsRequest; a string literal in handleAutomationRequest; a string literal in objectValueFailure; a string literal on a changed line))
  • content/docs/deployment/cli.mdx (via invalid_type (literal, a string literal in handleActionsRequest; a string literal in handleAutomationRequest; a string literal in objectValueFailure; a string literal on a changed line))
  • content/docs/permissions/system-context.mdx (via handleActionsRequest (symbol, a top-level function), handleAutomationRequest (symbol, a top-level function))
  • content/docs/protocol/objectui/concept.mdx (via invalid_type (literal, a string literal in handleActionsRequest; a string literal in handleAutomationRequest; a string literal in objectValueFailure; a string literal on a changed line))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via invalid_type (literal, a string literal in handleActionsRequest; a string literal in handleAutomationRequest; a string literal in objectValueFailure; a string literal on a changed line))
  • content/docs/releases/v17/17-6.mdx (via invalid_type (literal, a string literal in handleActionsRequest; a string literal in handleAutomationRequest; a string literal in objectValueFailure; a string literal on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 39ca58787acbadd3b6e28810e2baa8576bd0d243 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5d872fbfe2b3f11ccaac09f9a536484ca2f4fd69 — the merge of head e321fc54f209cbba4d1ea74fcb8639ea8e027a42 into base 39ca58787acbadd3b6e28810e2baa8576bd0d243, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 39ca58787acbadd3b6e28810e2baa8576bd0d243 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e321fc54f209cbba4d1ea74fcb8639ea8e027a42
Local-runs: none
Scope: packages/runtime/src/domains/actions.ts, packages/runtime/src/domains/automation.ts, packages/runtime/src/object-value-shape.ts, packages/runtime/src/domains/run-door-input-shape.test.ts, packages/runtime/src/domains/field-error-code-catalog.test.ts, packages/runtime/src/domains/automation-trigger-params-shape.test.ts + packages/runtime/src/endpoint-executor.ts (the type: 'flow' arm, line 579, the builder's second caller), packages/runtime/src/domains/activation-gate.ts (readActivationBody, line 232: the deps.error + VALIDATION_FAILED_STATUS + validationFailureDetails(validationFailure(...)) precedent the action door now copies), packages/runtime/src/error-envelope.ts (splitSemanticCode / buildApiError, the promotion of details.code into error.code), packages/runtime/src/validation-failure.ts and packages/types/src/validation-failure.ts (the constructor and the 400 constant), packages/spec/src/api/errors.zod.ts (FieldErrorCode, invalid_type) and packages/spec/src/api/error-code-ledger.zod.ts (VALIDATION_FAILED), packages/runtime/src/index.ts, packages/runtime/package.json and packages/runtime/tsup.config.ts (the package's public entry), scripts/check-route-envelope.mjs (DISPATCHER_DOMAIN_DIR, discoverDomains, the NOT-DECLARED refusal, the DISPATCHER_DOMAINS table), scripts/check-adr-0087-registration.mjs (findMigrationPrescription, readDisposition), .github/workflows/lint.yml (which job runs which gate), .changeset/25821-trigger-params-object.md (the sibling's prose face), docs/adr/0112-error-code-vocabulary-and-ledger.md, docs/adr/0114-field-level-error-code-catalog.md, .changeset/25976-run-door-input-shape.md (prose face, read for ②)

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 main carries at 05f7758f); PR #26034 body, file list (7 paths) and the net diff against main at this head (Accept: application/vnd.github.diff, 659 lines); the check-runs on the head, read twice (at open and at write time). Read-only: the diff, the local checkout at main and the gate scripts were read; nothing was built, run, re-run or ablated.

① Derived judgments

CI on the head at write time. 46 check-runs: 29 success, 11 skipped, 6 in_progress (Test Core shards 1, 2, 3, 4, 6 and 7 of 8), 0 failed, 0 cancelled. Concluded green: Build Core, Test Core 5/8 and 8/8, every Lint job (ESLint, docs & code guards, repo & tooling gates, CI/release & engine gates, PM dispatch-gates, the three scoped lanes, tooling self-tests, Lint & Repo Gates), every Type Check job (workspace, source gates, consumer gates, debt ledger, TypeScript Type Check), Check Changeset, Governed Surface Queue Guard, Dogfood Regression Gate, Check Documentation Links, Check PR Size, Flag docs affected, and the four card/branch/single-writer guards. The judgments below are read from the code; the six open shards are the dev's reported local run (runtime typecheck + test, 453 files / 7399 passed / 19 skipped at e321fc54) until they conclude.

a. The action door: which inputs are now refused, with what answer — right. actions.ts lines 688-713 (head) replace two truthy-object reads. The body: objectValueFailure('(body)', body ?? {}) refuses a string ("" included), a number (0 included), a boolean (false included) and an array ([] included); undefined and a JSON null body become {} through ?? {} before the check and keep the bodyless call. Before the diff, every one of those refused shapes was replaced by {} and the action ran (an array passed typeof and read as a body with no recordId and no params). The params key: objectValueFailure('params', reqBody.params) refuses a string, number, boolean, array and an explicit null; undefined (key absent) and any non-null non-array object, {} included, are admitted, and reqParams = reqBody.params ?? {} builds the bag an object params always built. Before, null/0/false/""/a string became {} and ran, and an array was handed on AS the bag. The answer is deps.error(message, VALIDATION_FAILED_STATUS, validationFailureDetails(validationFailure(message, [failure]))): details carries { code: 'VALIDATION_FAILED', fields: [{ field, code: 'invalid_type', message }] }, and buildApiError (error-envelope.ts line 118) promotes that details.code into error.code with httpStatus: 400, so the wire reads 400 / error.code === 'VALIDATION_FAILED' / error.details.fields[0] = { field: '(body)' | 'params', code: 'invalid_type', message: 'expected an object (a map of names to values), received ...' }. It is the exact spelling activation-gate.ts line 232 already uses for readActivationBody, so the action door's refusal rides the envelope the census test already pins for that door. Position: the hunk sits immediately after the flow-action 503 probe (line 684-686 on main) and before enforceActionParams, loadActionSubjectRecord and dispatch, which is where the PR body says it sits; a refused call reads no row and asks neither the automation service nor the script registry. The new suite pins execute and executeAction not called on every refused shape (9 params shapes x 3 actions, 8 body shapes x 2 actions) and pins the five accepted bodies ({params:{note:'x'}}, {params:{}}, {}, undefined, null) reaching both engines with the bag they always carried.

b. The trigger doors' body — right, and reaches exactly the wire doors. buildAutomationContext (automation.ts line 156) now reads objectValueFailure('(body)', body ?? {}) before ctxBody is typed, throwing validationFailure('Invalid trigger body — ...', [failure]). Refused: a string, number, boolean, array ("", 0, false, [] included); admitted: undefined, null (both become {}) and any plain object. Before, a string/number/boolean body became {} and dispatched the flow with an empty bag, and an array passed typeof and reached the flat-body branch, which forwarded its indices as params. I confirmed the caller set on main by grep: exactly two production call sites, respondToFlowTrigger (automation.ts line 2172, reached by POST /:name/trigger at line 3024 and the legacy POST /trigger/:name at line 2821) and executeFlow in endpoint-executor.ts line 579 (every declared type: 'flow' endpoint). No flow-to-flow node, scheduler or MCP path builds through it, so the body refusal cannot over-reach a non-wire caller. The throw is the same validationFailure the #25821 params refusal throws two lines below, on the same ride the sibling review 6410594343 traced to 400 VALIDATION_FAILED + details.fields at both dispatcher exits and endpointErrorAnswer; the 404 (flowIsUnknown) and 403 (flowStartRefusal) gates run before the builder at both call sites and are untouched by this diff. The suite pins both REST routes x 8 refused shapes with execute not called, both routes x 3 bodyless shapes (undefined, null, {}) dispatching with params: {}, and the declared endpoint through the real executeEndpointTarget (8 refused, 1 accepted).

c. The resume door: no observable change beyond the declared one — right. const rawBody = body ?? {} (line 3306 on main) is unchanged context, so the value handed to the check is never undefined and never null. On that domain the old predicate typeof rawBody !== 'object' || Array.isArray(rawBody) and objectValueFailure's value !== null && typeof value === 'object' && !Array.isArray(value) are the same truth table: a string, number, boolean and array refused, every plain object admitted, the bodyless resume kept. The top-level message is byte-identical: the new expected argument supplies an object with ${accepted} and the formatter appends , received ${received} with the same words the arm-local jsonTypeOf produced (null / an array / a ${typeof}). The one wire change is fields[0].message, which gains the , received ... suffix; field and code are unchanged, and the changeset declares it ("the resume door's fields[] message now also names the type received"). jsonTypeOf at line 3320 is still used by the branchLabel refusal (line 3419 on main), so the hoist leaves no dead local; ESLint concluded green.

d. No error code is minted. The top-level code is VALIDATION_FAILED (error-code-ledger.zod.ts line 302, in the standard list) and the field code is invalid_type (errors.zod.ts line 247, FieldErrorCode, ADR-0114); both are the codes the resume door and the #25821 refusal already emit. Nothing here is Clause-②: yes. The census test gains four rows for the two new emission sites, which is what ADR-0114's catalog test asks of a new fields[] emitter.

e. Public surface: unchanged. packages/runtime/package.json exports "." only, resolving to dist/index.*; tsup.config.ts has the single entry src/index.ts; src/index.ts does not export object-value-shape.js (grep over the barrel: no hit). objectValueFailure was a module-private function in automation.ts on main and is now a named export of a module no barrel re-exports, so the published .d.ts is identical before and after. No packages/spec/** path is touched; the claim's constraint holds. The MCP run_action door (action-execution.ts) is outside the diff and unchanged, as the acceptance note says.

f. Tests. The new suite drives the real HttpDispatcher and the real executeEndpointTarget, pins both directions at all three doors, and pins the three doors' fields[0] rows side by side with the resume sentence byte-for-byte. The PR records red-first (78 failed / 59 passed on base) and three single-anchor ablations each reproducing a red (17 / 36 / 33). I did not re-run them (read-only); the assertions as written would fail on the base code at every refused shape, which is the property the counts claim.

② Semver level

.changeset/25976-run-door-input-shape.md: '@objectstack/runtime': patch, title fix(runtime)!: ..., line-initial Clause-②: no (narrowing) — ..., line-initial Metadata-protocol: none (...), exactly one adr-0087: marker, not-required (no-migration-prescription), a BREAKING banner with Before / Now / Fix. The PR body carries the same two lines at line start (Clause-②: no (narrowing), Metadata-protocol: none). Measured against the diff: the only published change is a runtime accept-set narrowing at three request doors; no spec key, authorable value, stored shape or export moves, so Metadata-protocol: none is the right kind, and under AGENTS.md's level rule (none takes patch; a non-protocol breaking change ships patch and keeps its BREAKING banner and ADR-0087 disposition) patch is the right level. The marker's exemption holds on the gate's own test: findMigrationPrescription fires on a FROM/TO placeholder, an arrow rewrite under migration framing, a framed or header-framed rewrite table, or a framed removal; the changeset's **Fix.** line is a prose sentence with none of those, and the body has no migration heading, no arrow and no table, so no-migration-prescription is not self-contradicted. The disposition text is the sibling's (25821) word for word with the doors widened, and that changeset merged through the same gates. Note for the seat: check-adr-0087-registration.mjs runs for real only in cut-rc.yml (lint.yml runs its --self-test half under check:changeset-gate-self-tests), so on PR CI the marker is judged by reading; I have. Check Changeset concluded success on this head. The (narrowing) arm is what makes this record owed, and no (narrowing) is the measured value, departing from the claim's bare no exactly as the dispatch order asked. Convention check: no (narrowing) + Metadata-protocol: none + patch + BREAKING + not-required — all four present on the changeset, the two lines present on the PR body. Right.

③ Boundary flags

  • open_questions: [] in the os-dev-report. Nothing to answer.
  • a. Declared departure 1 — the action door's whole BODY, fixed in place. Judged as the brief asks. Same defect class: the body read body && typeof body === 'object' ? body : {} on main line 688 is the identical truthy-object coercion as the params read one line below it, on the same door, starting the same run with an empty bag; the card's business reason ("a caller that sends a wrongly shaped request still starts a run with no inputs") names it even though the card's Measured section probed only params. Mechanical: one more call of the same function with the same envelope, eleven lines in a file the claim's surface already holds. Declared with evidence: the PR body's "Departure from the claim's wording, declared" section, the live probe ("oops", [1,2], 42 as the body each 400 VALIDATION_FAILED at (body), the control runs listing only the controls), 16 refusal cases plus the census row in the tests, and the changeset's Clause-② line and Before / Now / Fix both name the body. Because it widens the narrowing beyond the card's literal "Wanted", it had to be on the published carrier, and it is. Accepted.
  • b. Declared departure 2 — packages/runtime/src/object-value-shape.ts, outside packages/runtime/src/domains/**. The gate rule exists as the dev states it: scripts/check-route-envelope.mjs line 962 DISPATCHER_DOMAIN_DIR = 'packages/runtime/src/domains', discoverDomains() (line 2283) lists every .ts there that is not .d.ts, .test. or .conformance., and lines 2565-2575 red any name absent from DISPATCHER_DOMAINS with "NOT DECLARED. Add it to DISPATCHER_DOMAINS". The alternative — declaring the helper in that table with handBuilt: 0, as activation-gate.ts is — exists but would be a false row: the table's own rationale for activation-gate.ts is that it answers refusals through deps.error, whereas this module writes no response at all and audits as nothing. So one sibling file beside validation-failure.ts in packages/runtime/src/, imported relatively by both domains, with no barrel export and no gate-table edit, is the minimal placement. The gate's job (Lint · docs & code guards, which runs pnpm check:route-envelope at lint.yml line 2814) concluded success on this head. The path is outside the claim's declared surface but inside the package and the lane, declared in the PR body's Acceptance notes and in the report. Accepted.
  • c. The dev's Clause-② departure from the claim's bare no. The measured value is right (② above); the claim said the dev measures and declares. Accepted.
  • d. Out-of-scope finding, main-red dogfood battery. test/authz-probe-blind-spot.test.ts reds on handleI18nRequest control count 3 vs 4; domains/i18n.ts is not in this PR's file list, and the dev's reading that origin/main carries 4 occurrences since the ETag change is consistent with the rest/i18n: GET /i18n/translations/:locale serves the whole locale bundle (288–361 KB raw, 80–88 KB compressed) on every console open with no ETag or Cache-Control, and carries Studio's metadataForms (46.8 KB) into the boot read #25785 ratchet note in DISPATCHER_DOMAINS (i18n.ts went to handBuilt: 1 for the ETag/304 face). Not this diff. The CI Dogfood Regression Gate on this head is success (it is not the full battery). To the seat: file it as its own card against the census file, with the dev's dedupe strings.
  • e. Out-of-scope note, MCP run_action. The same truthy-object guard in action-execution.ts is unreachable behind the tool's input schema (-32602 measured). Noted, not a card; agreed.
  • f. Docs drift. Six hand-written pages are flagged through the invalid_type literal. I read content/docs/automation/flows.mdx and content/docs/api/error-handling-server.mdx for the two doors: neither restates the old coercion of a trigger body or an action params (the only matches are the resume door's own invalid_type prose and the patch-row text about declarative updates), so no page is falsified by this diff. Advisory only.
  • g. Check-runs not success/skipped. Six in_progress at write time: Test Core 1/8, 2/8, 3/8, 4/8, 6/8, 7/8. None failed. Under the Tier S rule the seat lands only once they conclude green beside this record; this record judges the code and does not wait on them.

Implemented-by: claude/project-thread-jpr5ee
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-26T10:06Z

Head e321fc54, report 6413120939 (on #25976), claim 6411222289, contract review 6413351365 (PASS, narrowing face, by server seat 2).

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 10:07
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 10:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 3821ec8 Oct 26, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-jpr5ee branch October 26, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

automation: the action door drops a non-object params, and the trigger door accepts a non-object body (siblings of #25821)

2 participants