- Use a clean RailCall Station v0.78 workspace.
- Create a one-node workflow whose effect resolves to a harmless test integration:
- provider: fakepay
- verb: charge
- action_class: external_send
The test integration's apply() only appends to an in-memory list; it does not open a socket or contact a provider.
- Inject a policy callback that records calls and always returns:
{"decision":"block","reason":"blocked-by-control"}
- Control case: call
workflow_engine.plan_workflow()with that callback. - Call the real
workflow_engine.run_workflow()with the same workflow and the same blocking callback. - Repeat through the real
routes.dispatch_workflow._handle_dag_run()path withdry_run=false, live execution capability enabled, and a Team gate that returnsproceed.
Expected:
- A policy decision of
blockmust stop the workflow beforeinteg.apply(). - The global freeze is documented as a hard stop for every live workflow/connector send.
policy_gatemust be evaluated on the execution path, not only during an optional preview.
Actual:
plan.nodes[0].policy = {"decision":"block", ...}
plan.blast_radius.requires = "block"
policy_gate calls in plan = [("fakepay", "charge", "external_send")]
run.outcome = "COMPLETED"
policy_gate calls in run = []
effect marker = [{"amount_cents": 7}]
The direct /api/workflow/dag/run harness also returned outcome="COMPLETED" and reached the fake external-effect integration.
Control result:
workflow_mcp.stage_workflow()with the same workflow and blocking policy returnedok=falsewitherror="blocked by policy"and never called the effect.- Team approval was not the failing control; the Team gate returned
proceed.
Root cause:
workbench/routes/dispatch_workflow.py:681-684callsE.plan_workflow(engine_spec, signing=signing)withoutpolicy_gate.workbench/routes/dispatch_workflow.py:724-725callsE.run_workflow(...)withoutpolicy_gate.workbench/workflow_engine.py:621acceptspolicy_gate, butrun_workflow()never evaluates it before_run_node()invokesinteg.apply().workbench/studio_server.py:4139-4143documentspolicy_gateas a real choke point for every workflow/connector live send.
Impact:
An external-effect workflow classified block can reach the provider integration execution boundary through the direct DAG route. The harness intentionally stopped before any real network or financial write.
Suggested fix:
Run the same policy preflight used by workflow_mcp.stage_workflow() before direct DAG execution, and fail closed when the decision is block. Also enforce a blocking policy_gate inside run_workflow() when the parameter is supplied.
Station version (railcall version):
station-v0.78
Module slug + version:
N/A — Station workflow policy path
AFFECTED VERSION
station-v0.78
============================================================
TITLE
Direct /api/workflow/dag/run ignores declared workflow capability scope and executes an out-of-scope irreversible provider action on station-v0.78
BODY
VERDICT: CONFIRMED BUG
Reproduction steps:
- Use a clean RailCall Station v0.78 workspace.
- Store this engine spec as an installed workflow:
{
"id": "cap-bypass",
"capabilities": {
"providers": ["safe_provider"],
"allow_irreversible": false
},
"nodes": [
{
"id": "pay",
"type": "effect",
"action_id": "fakepay_charge",
"args": {"amount_cents": 1}
}
]
}
- Resolve
fakepay_chargeto a harmless local stub withaction_class="external_send"; itsapply()only writes an in-memory marker. - Call the real
routes.dispatch_workflow._handle_dag_run()withdry_run=false, live execution capability enabled, and Team gateproceed. - Control case: submit the same workflow to
workflow_mcp.stage_workflow().
Expected:
providersis an allowlist. A provider not in the list must be refused.allow_irreversible:falsemust refuse anexternal_sendaction.- Direct DAG execution must enforce the same declared scope as the workflow staging path.
Actual:
direct route outcome = "COMPLETED"
effect marker = [1]
plan systems_touched= ["fakepay"]
plan irreversible = ["pay:fakepay_charge"]
Control result:
workflow_mcp.stage_workflow() -> ok=false
error -> "capability exceeded: workflow touches ['fakepay'] but its declared providers are ['safe_provider']"
The MCP control refused before creating a run token. The direct DAG route executed the same out-of-scope effect.
Root cause:
workbench/workflow_mcp.py:137-175implements the declared capability checks forproviders,egress_hosts,max_spend_cents, andallow_irreversible.workbench/workflow_mcp.py:187-205invokes those checks during staging.workbench/routes/dispatch_workflow.py:681-725never calls_validate_wf()or_check_capabilities()before plan pin/run.workbench/workflow_engine.py:681-686enforces only the runtime spend field; it does not enforce provider, egress, or irreversible scope.
Impact:
A workflow can execute a provider and irreversible action outside the capability scope it declares. The harness used no network or financial provider; it proved the out-of-scope execution boundary was reached.
Suggested fix:
Apply the same capability validation to direct DAG runs before plan pinning and execution, or move capability enforcement into the shared engine so every execution surface receives identical scope checks.