← Community
bugfixed

capabilities.allow_irreversible:false is enforced only for effect nodes; an http POST node fires an irreversible action anyway (v0.99)

ShwetaShweta#115d ago · 38 views
affected: station-v0.99fixed in: station-v1.4.0

Reproduction steps:

  1. Author a workflow that DECLARES it performs no irreversible actions:

"capabilities": {"allow_irreversible": false}
(or a scoped "providers": [...] allowlist).

  1. Give it an http node that POSTs to an allowed domain — a mutating method, so

action_class is "external_send" (irreversible):
{"type":"http","policy":{"allow_methods":["POST"],"allow_domains":["data.example.com"]},
"request":{"method":"POST","url":"https://data.example.com/mutate","body":{...}}}

  1. Run it. Contrast with an effect external_send node under the same declaration.
  2. Run repro_allow_irreversible_capability_skips_http_node_v099.py against a clean

v0.99 extraction:
http POST node : executed=True ; capability_block=False
effect node : capability_block=True (correctly refused)

Expected:
A workflow that declares allow_irreversible:false is asserting a safety property —
"this rail performs no irreversible actions." An http POST/PUT/DELETE is
irreversible (external_send), so it must be refused under that declaration, the
same as an irreversible effect node. Likewise a node reaching outside the declared
providers allowlist must be refused whatever its kind.

Actual:
The declared-capability backstop (workflow_engine.py:882) is guarded to one node
kind:

if kind == "effect" and (_cap_providers is not None or _cap_no_irreversible):
_ci = R.resolve_node(n)
... if _cap_no_irreversible and _ci.action_class == "external_send": raise CapabilityExceeded

kind = n.get("type", "effect"), so an http node (type=="http") and a model
node never reach this check. An http POST — action_class external_send — executes
its outbound mutation with no capability check, while the identical guarantee
correctly refuses an effect external_send node (the repro shows exactly this). The
backstop was added specifically (comment #a960d0) because providers/
allow_irreversible "were checked at the staging surface only, so a direct run
executed out-of-scope irreversible actions the declaration forbade" — but the
engine-side fix is effect-only, so that exact gap (a direct run performing a
forbidden irreversible action) remains open for http/model nodes.

Note (distinctness): this shares its ROOT with finding #20 (the run_workflow node
loop gates multiple controls on kind=="effect", so http/model nodes bypass them),
but it is a DIFFERENT control with a different guarantee — #20 is the operator's
emergency global freeze; this is the workflow's OWN declared safety scope
(allow_irreversible / providers), violated with no freeze involved. A maintainer
fix should make the node loop cover every outbound kind for BOTH.

Suggested fix:
Apply the declared-capability backstop to every outbound node kind, not only
effect. For an http node, derive the action_class the same way plan_http does
(external_send for a mutating method) and refuse it under allow_irreversible:false;
map its target host/domain against the providers allowlist. Better, compute each
node's (action_class, target) uniformly and run ONE capability/providers/spend/
freeze gate over every node regardless of kind, so a new node type cannot silently
escape the workflow's declared scope.

5 pts

1 reply

Fixed in station-v1.4.0. The runtime capability backstop now classifies http nodes exactly as plan_workflow does and refuses an external_send (POST/PUT/PATCH/DELETE) under allow_irreversible:false — failing closed if unclassifiable — not just effect nodes.

Thanks for the report — credited.

Sign in to reply.