evidence_pack's spend-forgery fix (70b519) walls off output, so a verified workflow node receipt whose real spend lives only in output reports $0 in the compliance evidence pack (v1.3.0)
Reproduction steps:
- v1.3.0's evidence_pack fix (70b519) makes the executive-summary spend total
count ONLY from receipts whose signature verifies, and only from fields it
treats as "sealed" — _SPEND_SKIP_KEYS explicitly excludes "output"/
"result"/"post_result" from the spend walk, reasoning that module-supplied
content is forgeable (correct for the bug it targets: a module writing a
fake spend_cents into its own output to inflate the number).
- But
outputis not uniformly module-controlled — for a workflow "effect"
node, output is frequently the ACTUAL provider API response (e.g. a real
Stripe charge acknowledgment), and workflow_engine.py's OWN spend-cap
enforcement (run_workflow, ~line 1030) explicitly trusts exactly this:
candidate #2 of its 3-source spend-crediting logic is literally "Amount
surfaced in the node's output (Stripe charge response, payment gateway
acknowledgment)" via _find_amount_in(rec.get("output")). That credited
amount updates the run's internal spent_cents total (used to ENFORCE
capabilities.max_spend_cents) but is NEVER written back onto the node's
own receipt object, and routes/dispatch_workflow.py:_persist_dag_run_receipt
— which builds the exact JSON persisted to WS/receipts/runs/*.json that
evidence_pack.collect() reads — copies workflow_receipt, node_receipts,
outcome, error, routing, incremental from run_workflow()'s return
value, but never copies res["spent_cents"] (the run-level authoritative
total) either. So for a node whose handler reports spend only via output,
NO sealed field anywhere in the persisted, signed receipt tree carries the
dollar amount at all.
- Build a node receipt shaped exactly like a real, signed effect-node receipt
for stripe.charge_create with no dedicated spend_cents field, only
`output: {"amount_cents": 500000, "id": "ch_real_charge", "status":
"succeeded"}`; embed it in a properly-sealed workflow-run wrapper matching
_persist_dag_run_receipt's exact shape (own integrity_hash + Ed25519
signature over the WHOLE wrapper, per the v0.61 "seal the wrapper itself"
fix); feed it to evidence_pack.summarize().
- Run repro_evidence_pack_spend_undercounted_via_output_v130.py against a
clean v1.3.0 extraction. Output:
wrapper signature verifies: True
evidence_pack reported spend_cents: 0
real charge amount was: 500000 ($5,000.00)
Expected:
A cryptographically-verified receipt documenting a real, successfully-executed
$5,000 charge must contribute $5,000 to the evidence pack's spend total — the
whole point of gating the count on signature verification is so verified
numbers can be trusted, not so real spend can vanish. The evidence pack's
narrative explicitly claims the numbers are "counted only from receipts whose
Ed25519 signature verifies... a module cannot inflate them" — silent deflation
of REAL spend is just as much a violation of that claim as inflation, and
worse for a HIPAA/SOC2 evidence pack's actual purpose (proving what really
happened) than the forgery bug it replaces.
Actual:_sealed_actual_spend's blanket output/result exclusion, correct for
guarding against module-forged command receipts, is applied uniformly to
workflow node receipts too — where output is frequently the legitimate,
untamperable (it's inside the signed wrapper) record of what a provider
actually charged, and is explicitly the second-priority signal the engine's
own spend-cap enforcement trusts. Because the credited amount is never
propagated into a dedicated spend_cents field on the node receipt or the
run wrapper, evidence_pack has no sealed field left to read it from, and the
repro shows a verified $5,000 charge counted as $0.00 — the report is not
"honestly reporting less than it can prove" (the module_sandbox pattern this
platform otherwise follows), it is silently reporting the WRONG number as if
it were the true one, with no caveat in the summary or the basisnote.
Suggested fix:
Close the gap at the source rather than trusting output broadly: when
run_workflow credits spend from an output-surfaced or args-re-estimated
candidate (i.e. the node's own spend_cents/spent_cents field was absent or
lower than the credited max), write that credited amount back onto the node's
sealed receipt as its authoritative spend_cents BEFORE the receipt is
signed (workflow_engine.py, the same block that computes candidates/`max
(candidates)` around line 1030) — so the number the cap was actually enforced
against is the same number that ends up sealed and, later, counted. Evidence_
pack can then keep its output-exclusion policy unchanged and still see the
correct total, because the total now lives in a genuinely station-computed,
signed field rather than only inside the module/provider-shaped output
blob. As a stopgap, also have _persist_dag_run_receipt copy `res.get(
"spent_cents")` into the wrapper as a sealed field (it is already computed
authoritatively by the engine, before persistence, and costs nothing to
include in the hash), so evidence_pack has at least one reliable place to
read the true run-level total even before the per-node fix lands