Repository navigation
Description
Found while implementing #8796 (the resume body's outer envelope became a closed key set). Out of that card's scope: the ruling there covers unknown KEYS only; this is the same silent-drop family one axis over, on the VALUE of an accepted key, and widening refusals further is its own decision.
Measured on the resume branch, packages/runtime/src/domains/automation.ts
The signal assembly (deliberately field-by-field, per #3801) type-guards each accepted key and silently skips a value that fails the guard:
const inputs = (b.inputs ?? b.variables);
if (inputs && typeof inputs === 'object') signal.variables = inputs;
if (b.output && typeof b.output === 'object') signal.output = b.output;
if (typeof b.branchLabel === 'string') signal.branchLabel = b.branchLabel;So after #8796's closed set lands, POST .../resume {"inputs": "a string"} passes the key check (the key IS accepted), the value is dropped by the type guard, the engine receives an empty signal, and the caller gets HTTP 200 success:true with the submission treated as empty — exactly the failure shape #8796 measured for unknown keys, reachable through a well-spelled key with a mis-shaped value. Same for {"output": 42} and {"branchLabel": 7}. A non-object body (JSON string/number/boolean) and an empty-array body degrade the same way: they normalize to an empty signal and answer 200.
The decision this needs
- A — refuse a type-mismatched value on an accepted key (400, located, naming the key and the expected type), matching the toggle arm's treatment of
enabled(a truthy non-boolean is refused there, not coerced or dropped). - B — forward the raw value and let the engine judge, keeping the transport thin — but the engine contract types
variables/outputas objects, so this hands a service a shape its contract excludes (the same class finding:GET /api/v1/automation/:name/runs?limit=abccoerces to NaN and reacheslistRuns({ limit: NaN })— the same unvalidated query coercion #6928 fixed on the notifications route #7300 closed for?limit=abconlistRuns).
Option A is the shape the sibling arms already chose; filed rather than fixed because it widens refusals on an accepted public surface beyond the #8796 ruling's text.
Refs
- automation resume: the request body's OUTER envelope is lenient — an unknown top-level key is silently ignored and the submission is treated as empty #8796 — the outer envelope's closed KEY set (this is the value axis of the same defect family)
- screen flows: resume performs no server-side validation — missing required fields and undeclared keys all complete the run #4477 — the inner bag's declared=enforced treatment
- 请求体从不与声明它的 schema 对照(#3877 的请求侧对偶):7 个 schema 定义了从未启用,而 API 目录已宣称生效 #3899 — toggle arm precedent: a truthy non-boolean
enabledis refused, not dropped
Generated by Claude Code
Activity
os-zhuang commentedon Aug 18, 2026
Triage: lands in
packages/runtime/src/domains/automation.ts⇒domain:cli, type Bug,pm:queue— enqueued under the standing meta-criterion (maintainer, 2026-08-07): a silent-drop surface joins the refusal set its sibling limbs already ruled loud, inheriting the #8796 ruling (closed key set, loud refusal) together with its reason, plus the #3899 toggle-arm precedent (truthy non-booleanenabledis refused, not coerced). The value axis carries no semantic difference that would reopen the ruling: the engine contract already typesvariables/outputas objects, so refusing a mis-shaped value is declared=enforced restoration, not a new contract. Ruled route: Option A — 400, located, naming the key and expected type; non-object and array bodies refuse the same way. Veto window: this round's report on #6015.CONTRACT_REVIEW_TIER), and the claim comment must carryClause-②: yes.Triage seat routine — session
session_pm_triage_20260818T0104Z.Generated by Claude Code
os-zhuang commentedon Aug 18, 2026
Claim: PM loop round 19
Session:
session_012WKSnqAaoqtW3QX7SSf1VkBranch:
claude/issue-9416-resume-value-type-refusalWorktree:
objectstack-issue-9416Domain:
domain:cliFile surface, declared to REGION level (required by the same-package exemption):
packages/runtime/src/domains/automation.ts— the resume signal-assembly block only (theinputs/variables/output/branchLabeltype-guard sequence and its refusal path), plus test siblings. ⛔ NotrespondToFlowTrigger, ⛔ not the toggle arm, ⛔ notflow-dispatch-status.ts.Container & model:
M,mode:subagent,model: opus— DOWNGRADE under the maintainer's one-time Clause-② authorization (verbatim 「授权一次性降档到 opus」, naming this card among five).Clause-②: yes — triage wrote it into the card; Option A refuses a request shape that is accepted today.
⭐ Serial constraints — first dispatch under the NEW same-package exemption
Maintainer, 2026-08-18, verbatim: 「放开同包,同文件仍硬串行」. This supersedes the 2026-08-13 refusals and this morning's re-confirmation, and it was granted on evidence measured today: #9460 and #9446 were developed concurrently in
packages/runtime(different files) and merged cleanly at 11:43:09Z and 12:28:20Z with no conflict.⇒ This card runs alongside in-flight
packages/runtimework — #9462 (endpoint-executor.ts), #9457 (vitest config), #9568 (dispatcher-error-vocabulary.ts). All four declare disjoint region surfaces. ⛔ Same-file remains hard serial and none of these four share one.The exemption buys queueing, not collision-safety, so the four alternative rules apply and are in the dispatch: ① region-level file surface (above) · ② merge
mainonce before opening the PR · ③ merge again after a sibling card lands · ④ conflicts go to the merge queue, ⛔ the PM does not hand-order them. ⛔ The exemption does not spread to other cards.Premise check on⚠️ Locate it by text, not by the card's line numbers: this file has absorbed #8684, #8796 and #9446 since the card was written.
origin/main:domains/automation.tslast moved with #9446 (ca2e020e4), which touched the flow-trigger responder and not the resume assembly — so this card's block is intact.Generated by Claude Code
fix(runtime): resume refuses a type-mismatched value on an accepted k…
os-zhuang commentedon Aug 18, 2026
{ "issue": 9416, "status": "done", "branch": "claude/issue-9416-resume-value-type-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/9685", "premise_still_valid": true, "summary": "Option A implemented in the resume signal-assembly block of packages/runtime/src/domains/automation.ts, located by text (the file had absorbed #8684/#8796/#9446). Two refusals now wrap the field-by-field assembly: the body itself must be a non-array object (a JSON string/number/boolean and an EMPTY array previously normalised to {} and answered 200 on an empty signal), and each accepted key must carry a contract-shaped value — inputs/variables/output an object, branchLabel a string — each offending key getting its own invalid_type entry naming the key, the expected type and the type received. Both reuse the file's own validationFailure helper and the ADR-0114 catalog members the sibling arms already answer with; no new envelope, no new code, no spec widening. Two deliberate calls worth review: null IS refused (JSON can express it, and it was the value dropped most quietly), while a key whose value is `undefined` counts as ABSENT (JSON.stringify drops it, so no HTTP caller can produce one — over the wire this is indistinguishable from the toggle arm's `in`-based presence test; it differs only for in-process callers, where `{ inputs: maybeUndefined }` means 'no inputs'). Arrays are refused for inputs/output too: ResumeSignal types them Record<string, unknown>, which excludes arrays, and the old `typeof === 'object'` guard forwarded them — the exact Option B failure the ruling rejects. Assembly is unchanged in substance: never a body spread, so #3801's symbol-keyed service-authority marker stays unforgeable. Merged origin/main once before opening (clean, main had not touched automation.ts); stayed inside the declared region — respondToFlowTrigger, the toggle arm and flow-dispatch-status.ts untouched.", "tests": "All at HEAD 155e19388, the final commit; union re-derived from the ACTUAL changed paths with `node scripts/pm/dispatch-gates.mjs` and re-derived again after the main merge. TARGETED: `pnpm --filter @objectstack/runtime typecheck` clean; the two resume test files `Test Files 2 passed (2) / Tests 51 passed (51)`; full `@objectstack/runtime` `Test Files 171 passed (171) / Tests 2569 passed (2569)`. ABLATION (restore the silent-skip guards via `git checkout HEAD~1 -- packages/runtime/src/domains/automation.ts`, the branch point being exactly the pre-fix file): PREDICTED BEFORE RUNNING 30 red / 11 green in the new file with the #8796 sibling file staying 10/10 green; ACTUAL `Tests 30 failed | 21 passed (51)`, `Test Files 1 failed | 1 passed (2)` — exact match on both numbers and on the file split. The one new-file test predicted green is the ordering pin 'reports an unknown KEY ahead of a mis-shaped value' (the #8796 unknown-key arm survives the ablation, so it cannot redden for this defect and is not claimed to); the non-empty-array test DID redden because its locator moves from the key check back to `(body)`. Restored with `git checkout claude/issue-9416-resume-value-type-refusal -- <path>`; post-restore diff against the branch empty. GATES, all green: check:route-envelope (16 dispatcher domains audited, 0 new ratchet), check:cross-package-test-inputs, check:changeset-gate-self-tests, check:objectui-changeset, check:nul-bytes (plus a manual control-byte grep over the three changed files), check-adr-0087-registration (1 declared-breaking changeset, disposition present), check-changeset-no-major, check-empty-changeset, check-cross-package-test-inputs, docs-audit/check-affected-docs. CONVENTION-TRIGGERED by the new test file, none named in the dispatch and all found by re-derivation: check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, and check:type-check-debt — the load-bearing one: packages/runtime/tsconfig.json EXCLUDES **/*.test.ts, so `tsc --noEmit` never read the new file, and the package carries a 227-error TEST_DEBT entry that a new hidden test file can push up. Ran it the way CI does, with the workspace closure built first (turbo build, 70/70 tasks): '33 ledger entries re-measured, none above its recorded number. surplus: none' — the new file contributes ZERO new type errors. CONSUMERS, prefix direction `...@objectstack/runtime` (downstream), stated because the direction is what makes the claim checkable: @objectstack/client 23 files / 310 tests, @objectstack/hono 2 / 73, @objectstack/http-conformance 4 / 72, and @objectstack/dogfood test/flow-durable-suspend.dogfood.test.ts 1 / 11 — the only consumer in the repo that POSTs a real resume body over HTTP. All green; no consumer fixture flipped. CI JOBS at report time, read once without waiting (maintainer 2026-08-10): TypeScript Type Check in_progress, Lint & Repo Gates in_progress, Test Core (1-3/3) in_progress, Dogfood Regression Gate (1-3/3) in_progress, Build Core in_progress, Temporal Conformance (live PG + MySQL) in_progress. No gate had reported a conclusion when this report was filed.", "open_questions": [], "out_of_scope_findings": [] }Zone 2 assumptions — both falsified-checks came back confirming, one with evidence worth keeping:
route-ledger.tsregistersPOST /automation/:name/runs/:runId/resumeonce;dispatcher-plugin.tsforwardsreq.bodyintodispatcher.dispatch→handleAutomation→ this arm;packages/resthas no resume route at all (the approval decision path runs throughApprovalServicein-process and never touches this body assembly). All six shapes enter at the same two statements, so this is not a half-fix.validationFailurecan express 'expected type' per key." Confirmed, no widening.invalid_typeis already a member of the closedFieldErrorCodecatalog inpackages/spec/src/api/errors.zod.ts, and the per-fieldmessagecarries the expected type — which is exactly what the toggle arm next door already does.packages/specwas not touched.Not filed as issues, but flagged for the reviewer (neither is a defect and both are already-known state, so filing would be noise):
packages/runtime/tsconfig.jsonexcludes**/*.test.ts, so the package'stypecheckscript does not read its own tests — the AGENTS.md hazard. It is pre-existing, already ledgered as the 227-errorTEST_DEBTentry with its tracking link, out of this card's declared region, and the re-measure above proves this PR adds nothing to it.'enabled' in bodywhile this arm uses!== undefined. Over HTTP the two are identical (JSON cannot produce anundefinedvalue); the divergence is documented in the code comment rather than silently taken.Generated by Claude Code
fix(runtime): resume refuses a type-mismatched value on an accepted k…