fix: gate act stage on act.ok (mirror prove-side failure handling) - #42
Conversation
…failure handling The orchestrator was marking stages.act.status as 'passed' on every successful runAct return, even when runAct returned ok:false with parseable JSON on stdout (a structured act failure). The prove path already gates on prove.ok; the act path was the asymmetric one. A structured act failure now: marks act as 'failed', sets exitCode=1, records prove as 'skipped' with reason 'act_failed', and updates the flow string to 'decide -> act -> stop (act failed)'. Adds a regression test paralleling the existing 'nonzero prove JSON' test (test/stack.test.mjs:568). Refs: eaudoon-audit/findings/agent-action-stack.md FINDING-20260911-01
| }); | ||
| const outcome = act.raw?.outcome ?? null; | ||
| stages.act = stageRecord("passed", { raw: act.raw }); | ||
| stages.act = stageRecord(act.ok ? "passed" : "failed", { raw: act.raw }); |
There was a problem hiding this comment.
🟡 Act failure diagnostics are discarded
When runAct returns ok: false, its stderr is omitted from both the report and manifest. Human output then shows no child diagnostic for the failed act.
Learn more
Structured child failures return stderr separately from their JSON payload through runAct. The prove path clips that field and stores it in both the stage record and report, making it available to the manifest and human output. The new act failure path changes the stage to failed but stores only raw, so persistRunBundle receives no stderr and printHuman has nothing to display. The regression test supplies stderr: "rail: failure", but no assertion checks its persistence.
Example: An act child exits with status 1, JSON { "error": { "code": "RECEIPT_FAILED" } }, and stderr rail: failure. The bundle records the failed act and its JSON artifact, but manifest.stages.act.stderr is null and CLI output omits act_stderr.
Recommended fix: Mirror the prove path: compute clipChildStderr(act.stderr), spread it into stages.act, and include it in report.stages.act. Extend the regression test to assert the report and manifest values.
Was this helpful? React with 👍 or 👎 to provide feedback.
What
The orchestrator in
bin/aas.mjswas markingstages.act.statusas"passed"on every successfulrunActreturn, even whenrunActreturned{ ok: false, raw: payload, status: <non-zero> }— i.e., a structured act failure with parseable JSON on stdout. The correspondingrunProvepath correctly gates onprove.ok; the act path was the asymmetric one.Concrete impact: a structured act failure (e.g. consequence-rail CLI exiting non-zero with parseable error JSON) gets persisted as
act: passed,exitCodestays 0, and the prove stage runs anyway. End result: a fully failed act can produce apassed-act + passed-prove + exit 0run bundle — durable state mislabeled.Fix
In
bin/aas.mjs:1599-1622, mirror the prove-side pattern:stages.act.statusonact.okexitCode = 1on!act.okstages.proveasskippedwith reason"act_failed"report.flowto"decide -> act -> stop (act failed)"Test
Added a regression test in
test/stack.test.mjsparalleling the existing "nonzero prove JSON is recorded as a failed proof" test (line 568). The new test forcesrunActFnto returnok:falseand asserts:result.exitCode === 1result.manifest.stages.act.status === "failed"result.report.stages.act.status === "failed"result.manifest.stages.prove.status === "skipped"result.report.stages.prove.reason === "act_failed"result.report.flow === "decide -> act -> stop (act failed)"Severity
HIGH (sensitive 4, asset bump +1). Routing per audit bar: PR + maintainer review, NOT auto-merge. The tier consideration was HIGH vs CRITICAL — calling HIGH because the raw payload is preserved in the bundle (data not lost, only mislabeled); CRITICAL was a strict reading of the bar's "mis-represent durable state" clause.
Verification
node --test test/stack.test.mjs→ 94/94 pass (was 93, +1 new regression test)node --test test/gui.test.mjs→ 45/45 passok:falserunActFninjection) flips all six label/exit-code fields correctly. See cycle-2-status report for before/after table.Out of scope
No public API change. The
runActFninjection point is unchanged; therunActwrapper contract is unchanged. Only the orchestrator's interpretation of{ ok: false, ... }flips to mirror the prove side.Refs: FINDING-20260911-01 in the audit ledger.