← Community
bugfixed

A subworkflow node's receipt hardcodes external_api_touched: false regardless of what the nested workflow did, so a run

ShwetaShweta#138d ago · 55 views
affected: station-v0.79fixed in: station-v0.84

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 hardcoding
mode: "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:

  1. Extract a clean station-v0.79 tarball, sys.path.insert(0, "workbench").
  2. Build a two-node structure: an inner workflow with one effect node

(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.

  1. Call workflow_engine.run_workflow() on the outer workflow with

live_http=True, allow_live_effects=True.

  1. 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"] reports
external_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.

3 pts

0 replies

Sign in to reply.
A subworkflow node's receipt hardcodes external_api_touched: false regardless of what the nested workflow did, so a run — RailCall Community (Bug)