A subworkflow node's receipt hardcodes external_api_touched:
false regardless of what the nested workflow did, so a run that genuinely
executed a live effect one level down gets sealed as mode="dry"
v0.78 added a real fix to the DAG-run receipt: instead of hardcodingmode: "live" unconditionally (so a sealed dry-run receipt could read as
live -- a false claim the signature then made tamper-evident but still
false), the run's mode is now derived from whether any of its node
receipts actually show mode == "live" or external_api_touched: true.
That fix has a blind spot for the subworkflow node kind: a subworkflow
node's own receipt hardcodes external_api_touched: false no matter what
the nested workflow actually executed, and the nested run's own node
receipts are never surfaced into the parent's node_receipts list (only
referenced by hash). A parent workflow made of nothing but a subworkflow
node that itself fires a real, live financial transaction gets its own
top-level run sealed with mode: "dry" -- the exact false claim the
v0.78 fix exists to prevent, reintroduced through a sibling node kind the
fix never accounted for.
Reproduction steps:
- Extract a clean station-v0.79 tarball, sys.path.insert(0, "workbench").
- Build a two-node structure: an inner workflow with one
effectnode
(provider stripe, action_id stripe_charge_create) that genuinely
executes in live mode (verified independently: running it standalone
produces a node receipt with mode="live", external_api_touched=true),
and an outer workflow whose only node is a subworkflow node wrapping
that inner workflow.
- Call workflow_engine.run_workflow() on the outer workflow with
live_http=True, allow_live_effects=True.
- Inspect the outer run's node_receipts, then apply the exact check
routes/dispatch_workflow.py's dag/run handler uses to derive the run's
mode field: any(nr.get("mode")=="live" or nr.get("external_api_touched")
for nr in node_receipts).
Expected: the outer run's node_receipts reflect that a live effect
occurred somewhere in the run, and the sealed mode field reads "live".
Actual: the outer run's only node_receipts entry is the subworkflow
node's own receipt, which reads external_api_touched: false
unconditionally -- even though the nested workflow it wraps genuinely
executed a live Stripe charge (confirmed by running that same inner
workflow standalone, which correctly reports mode="live",
external_api_touched=true). The dag/run handler's mode-derivation check
therefore computes "dry" for a run that actually moved real money.
Root cause: workbench/workflow_engine.py's subworkflow node-kind branch
in _run_node() builds its receipt as
`{"schema": "railcall_subworkflow_receipt.v1", ...,
"external_api_touched": False, ...}` unconditionally, never inspecting
whether the nested run_workflow() call it just made
(`sub_res = run_workflow(sub_spec, ws=ws, signing=signing,
live_http=live_http, allow_live_effects=allow_live_effects)`) actually
touched anything live. The nested run's own per-node external_api_touched
values are available on sub_res["node_receipts"] but never consulted.
Suggested fix: set the subworkflow node's own external_api_touched to
true if any node receipt inside sub_res["node_receipts"] reportsexternal_api_touched: true (or sub_res's own workflow receipt records
any live activity), so a live effect nested inside a subworkflow is
visible to any caller -- including dispatch_workflow.py's mode-derivation
fix -- that only inspects the immediate node_receipts list rather than
recursing into subworkflow receipts.