← Community
bugfixed

New routing-policy block is reported as a successful empty answer by the offload endpoint, and is never receipted or audited

ShwetaShweta#121d ago · 57 views
affected: station-v0.83fixed in: station-v0.86

station-v0.83 adds a genuine governance control to the orchestration router:
execution_policy's routing.local_only and routing.sensitive_stays_local,
enforced in route_executor, which BLOCK a unit rather than let it reach a cloud
model. The block works -- nothing is sent to a provider. This report is not
about a bypass.

The defect is that the new "blocked" verdict is a governance decision, and only
one of its three call sites treats it as one.

The workflow "model" node handles it correctly: _run_model_node raises, so
the node fails and the saga rolls the run back -- the same fail-closed
contract as an airlock deny.
POST /api/model/offload -- the endpoint behind the railcall_offload MCP
tool -- does not. Its response builder reads only reply/text/content from
the result, and a blocked result carries none of those, so it returns
{"ok": true, "answer": "", "served_from": "blocked"}. The block reason,
which the executor computed and put in the result's error field, is
dropped. A caller cannot distinguish "the operator's policy refused this"
from "the model returned nothing." That matters most for the MCP tool,
whose whole purpose is for an assistant to hand over bulk work
unsupervised. (The items batch form does surface a served_breakdown
containing "blocked", so it carries partial signal; the single-input form
carries none.)
* station_llm.complete()'s routing path returns the blocked dict to its
caller and returns before any receipt is built.

Second facet, same root cause: the refusal leaves no provable evidence. A
governance block writes zero signed receipts and zero audit_log entries. The
only trace is one row in route_ledger.jsonl, which is the unsigned
savings-metrics file, not an audit record. station_llm.complete()'s own deny
path does the opposite -- it signs a denied receipt and writes an
egress_denied audit entry, with the comment "the whole point of enforcement:
provable refusals." The new routing block is the same class of decision and
produces none of that, so an operator who turns the control on cannot later
demonstrate that it ever fired.

Reproduction steps:

  1. Extract a clean station-v0.83 tarball.
  2. Write WS/execution_policy.json with

{"routing": {"sensitive_stays_local": true, "local_only": true}}

  1. With no local model reachable, call route_executor.execute() with

task_type="classify" and any message. Confirm the route reports
served_from="blocked" with policy_blocked=true and a reason string.

  1. Apply the same result-shaping the offload handler uses -- read

reply/text/content off the result -- and observe what a caller receives.

  1. Count files in WS/receipts/egress/ and check for WS/audit_log.jsonl.

Expected: step 4 surfaces the refusal to the caller (ok:false, or an explicit
blocked field carrying the reason); step 5 shows a signed receipt and an audit
entry recording that policy refused the unit.

Actual: step 4 yields {"ok": true, "answer": "", "served_from": "blocked"} --
the reason is discarded and the call reads as a success that produced nothing.
Step 5 shows zero receipts and no audit_log at all; the only record is an
unsigned route_ledger row.

Root cause: route_executor's _blocked() returns
{"error": <reason>, "blocked_by_policy": true} as the result. The model-node
caller checks route.policy_blocked and raises, but _handle_model_offload in
routes/dispatch_workflow.py reads only reply/text/content and always responds
ok:true, and neither _blocked() nor its callers emit a receipt or an audit
record for the decision.

Suggested fix: treat a blocked route as a refusal at every call site rather
than at one. In the offload handler, check route.policy_blocked (or
served_from == "blocked") and respond with ok:false plus the reason, matching
what the model node already does; and emit a signed refusal receipt plus an
audit_log entry from the block path itself, so the guarantee is provable the
same way a policy deny in station_llm.complete() already is.

5 pts

1 reply

Fixed in station-v0.86. Sharp catch, @dinkarshweta — a routing-policy block was surfaced as a successful empty answer instead of a refusal. The model-offload path now detects the block (blocked_by_policy / policy_blocked / served_from == "blocked") and returns {ok:false, blocked_by_policy:true} with the reason, so a governed block reads as a block, not an empty success. Credited.

Sign in to reply.