Skip to content

fix(automation): the resume body's outer envelope becomes a closed set — unknown top-level keys are refused - #9419

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8796-resume-envelope-closed-set
Aug 18, 2026
Merged

fix(automation): the resume body's outer envelope becomes a closed set — unknown top-level keys are refused#9419
os-zhuang merged 3 commits into
mainfrom
claude/issue-8796-resume-envelope-closed-set

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8796

Maintainer ruling 2026-08-15 (Option A, verbatim on the issue: 「接受你的所有建议。」): the resume route's request body becomes a closed set — exactly inputs / variables / output / branchLabel. An unknown top-level key is refused with a located 400 naming the offending key(s) and the accepted set.

What was measured (premise re-verified on bc6434b5b, post-#8684)

POST .../runs/:runId/resume with {"nodeId":"ask","values":{...}} — no key of which the route reads — answered HTTP 200 success:true with the screen submission treated as empty: the run completed and the submitted value never reached the flow. Re-measured by reverse verification from the committed fix: with packages/runtime/src/domains/automation.ts restored to origin/main, all 5 refusal pins in the new test file go red in exactly that shape (engine called with an empty signal, 200), while all 5 preservation pins stay green. The premise held after #8684 landed; the accepted set is still exactly the four keys (walked every body read in the resume arm — nothing else is consumed).

The refusal's code — deliberately NOT FLOW_FAILED

The refusal is thrown as the duck-typed validation failure both dispatcher error exits map to 400 + error.code: 'VALIDATION_FAILED' + details.fields[] (#3918; dispatcher-validation-error.test.ts pins that mapping end-to-end for both exits) — the same wire shape the toggle arm's closed body set in this same file already answers, and a code the ADR-0112 ledger already registers for @objectstack/runtime. Reasoning, per the objectui contract recorded on #8684 (PR objectui#4899): the console treats 400 FLOW_FAILED as terminal — the engine consumed the suspension and the run actually ran. This refusal is the opposite case: the request never reaches the engine, the suspension is untouched, and the caller can legitimately retry with a corrected body. It therefore sits with INVALID_SIGNAL / INVALID_SCREEN_INPUT on the retryable side, and reusing FLOW_FAILED would have told the console a live run is dead. Each details.fields[] entry carries the ADR-0114 catalog code unknown_field.

Seam and ordering

Wire shape, pinned

New test file packages/runtime/src/domains/automation-resume-envelope.test.ts (10 tests), following the automation-body-validation.test.ts precedent:

  • Refusal pins — the measured GA body, a single guessed values, a half-wrong body (valid inputs beside an unknown sibling — Option B was explicitly declined), and a non-empty array body: each names every offending key and the full accepted set, carries unknown_field field entries, is VALIDATION_FAILED (asserted ≠ FLOW_FAILED), and the service is never called.
  • Preservation pins — all four accepted keys forwarded byte-for-byte; the variables alias; empty object / undefined / null bodies still resume with an empty signal (a legal empty submission for a screen whose declared fields are all optional).
  • Escape pin — the refusal escapes dispatch() as the exact shape both HTTP error exits recognise (validationFailureDetails), with the service never called.

Known residue, filed rather than fixed (out of the ruling's scope — it covers unknown keys): a type-mismatched value on an accepted key is still silently dropped; recorded as #9416, which remains open and is not addressed here.

Verification (local, at head ef513cf07, after merging main @ 40d5b2d4c)

  • @objectstack/runtime full suite: 166 files / 2477 tests passed — including every automation: the run-resume route still answers HTTP 200 wrapping an inner {success:false} — the route #3962's status-code unification left behind #8684 pin (none moved) and the 10 new pins.
  • pnpm --filter @objectstack/runtime typecheck (tsc --noEmit): clean.
  • Reverse verification from the committed state: source restored to origin/main → 5 refusal pins red / 5 preservation pins green → restored; direction as expected (refusals turned red because the lenient envelope accepted the bodies).
  • Gates re-derived from the real diff (scripts/pm/dispatch-gates.mjs) and run at head: check:cross-package-test-inputs, check:route-envelope, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, docs-audit/check-affected-docs, check:nul-bytes, plus the convention-triggered test family the derivation added over the dispatch list: check:query-options-erasure, check:type-check-coverage, check:type-check-debt (workspace closure built first), check:engine-double-contract, check:where-matcher — all green.

Changeset

BREAKING framing, shipped as minor per the launch-window lockstep rule (check-changeset-no-major prescribes it; every publishable package is one fixed group). ADR-0087 disposition answered in writing: not-required (no-migration-prescription) — no metadata surface changes shape; the only channel that reaches raw-HTTP callers is the changeset itself.


Generated by Claude Code

The resume route's outer envelope becomes a closed set — exactly
`inputs` / `variables` / `output` / `branchLabel` (maintainer ruling
2026-08-15, Option A on #8796). An unknown top-level key is refused with
a located 400 naming the offending key(s) and the accepted set, thrown
as the duck-typed validation failure both dispatcher error exits map to
400 VALIDATION_FAILED + fields[] (#3918) — the same wire shape the
toggle arm's closed set answers, and deliberately not FLOW_FAILED: this
refusal never reaches the engine, the suspension stays intact, and the
caller can retry with a corrected body.
The refusal wraps #3801's field-by-field signal assembly unchanged, and
runs before the service call (#3899: nothing reaches the engine until
the body is legal).
Co-Authored-By: Claude <noreply@anthropic.com>
… the launch-window lockstep rule
Every publishable package is in one Changesets fixed group, so a single
major promotes the whole monorepo; check-changeset-no-major prescribes
minor for a breaking change during the launch window. The body keeps the
BREAKING framing and answers the ADR-0087 disposition in writing.
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx(via unknown_field (literal))
  • content/docs/automation/approvals.mdx(via nodeId (literal))
  • content/docs/automation/flows.mdx(via branchLabel (literal), nodeId (literal))
  • content/docs/kernel/cluster.mdx(via nodeId (literal))

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

  • content/docs/releases/v17.mdx(via nodeId (literal), unknown_field (literal))

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

Coarse fallback — 21 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 origin/mainpackageMentionDocs.

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 origin/main → pass the list as args.docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude