← Community
bugfixed

Unattended runs self-approve a require_human plan via the pin's unsigned approved_by field, while the verifiable approval_receipt is ignored

ShwetaShweta#112d ago · 28 views
affected: station-v1.0.0fixed in: station-v1.4.0

The guarantee that an unattended (scheduled) run may not self-approve a plan
whose blast radius requires a human rests entirely on one unsigned free-text
field in a local JSON file. The same file carries a reference to the signed
receipt that would actually prove a human approved, and the gate never reads it.

The live-run gate (routes/dispatch_workflow.py:905-908) decides whether a
scheduler-channel run may proceed on a require_human plan:

_pin_rec = _pp.load(WS, wf_id) or {}
_pin_human = bool(_pin_rec.get("plan_root")) and not str(
_pin_rec.get("approved_by") or "").startswith("migration:")
if not (channel == "scheduler" and _pin_human):
return _refused(... "an unattended run may not self-approve it" ...)

So "a human approved this" reduces to "the pin's approved_by does not start with
'migration:'". The pin (primitives/plan_pin.pin) is written as plain JSON with no
signature and no integrity hash:

rec = {..., "plan_root": ..., "approved_spend_cents": ...,
"approved_requires": ..., "approved_by": approved_by,
"approval_receipt": receipt_id, ...}
json.dump(rec, fh)

Two fields in that record govern the run, and only one of them is protected.
plan_root IS integrity-anchored: plan_pin.check() compares it to the freshly
recomputed, Ed25519-signed plan.workflow_root, so an attacker cannot change WHAT
runs without breaking the match. approved_by — the field that alone separates a
human approval from an un-reviewed migration auto-pin — is anchored to nothing.
And approval_receipt, the pointer to the signed approval receipt that could
prove the human decision, is written (plan_pin.py:98) and read nowhere in the
tree (a whole-repo grep finds that one write and no read).

Flipping approved_by from "migration:first-live-run" to any string not starting
with "migration:" — leaving plan_root, and therefore the signed-plan match,
untouched — makes _pin_human true, and the scheduler channel then self-approves an
irreversible / require_human workflow: exactly the action this gate exists to
forbid. plan_pin.check() continues to report "matches the approved plan", because
the root match is sound; it proves WHAT runs, never WHO approved it.

This is the same class as the staged-integration-send seal gap: a governance
record in which one field is integrity-anchored and the sibling field the gate
actually trusts is not, with a correct, verifiable value present in the record
(here approval_receipt) that the enforcement declines to use.

Reproduction steps:

  1. Extract the station-v1.0.0 tarball to a clean directory and put workbench/

on sys.path.

  1. plan_pin.pin(ws, "wf", plan, approved_by="migration:first-live-run",

acknowledged_migration=True) — the state the first live run leaves.

  1. Replay the gate predicate: load the pin and compute _pin_human = bool(

plan_root) and not approved_by.startswith("migration:"); confirm a scheduler
run is refused.

  1. Open <ws>/plan_pins/wf.json, set "approved_by" to "operator:jane" (leave

plan_root and everything else unchanged), save.

  1. Recompute the predicate, and call plan_pin.check(ws, "wf", plan).

Expected: whether a plan counts as human-approved for an unattended run is bound
to a verifiable artifact — the signed approval receipt the pin already
references — so relabeling a text field cannot manufacture a human approval.

Actual:
step 3 approved_by="migration:first-live-run" -> scheduler allowed = False
pin has signature: False; approval_receipt: None (never consulted)
step 4/5 approved_by="operator:jane", plan_root unchanged
-> scheduler allowed = True (require_human plan runs unattended)
plan_pin.check() -> True, "matches the approved plan"

Root cause: the pin conflates "this is the approved plan" (plan_root, effectively
signed via comparison to the signed plan) with "a human approved it" (approved_by,
an unsigned free-text label), and stores but never verifies the one field that
could bind the second claim to reality (approval_receipt).

Suggested fix: bind the human-approval decision to the signed approval receipt
the pin already carries — on the unattended path, load approval_receipt, verify
its Ed25519 signature against the install key, and confirm it attests a human
channel (session/cli) approving THIS plan_root — rather than trusting the
approved_by string. At minimum, sign the pin record (Ed25519 over the whole
record minus the signature, as every other governance artifact in this tree is
sealed) so approved_by cannot be edited without detection. The "migration:"
prefix convention is not an integrity control and should not be treated as one.

5 pts

1 reply

Fixed in station-v1.4.0. plan_pin.pin now seals the governance fields (plan_root, approved_by, approved_requires, approved_spend_cents, migration flag) with a station Ed25519 signature; the unattended gate calls plan_pin.human_approved, which verifies that seal against the pinned install pubkey. A flipped approved_by no longer verifies; an unsigned/old/migration pin fails closed (re-approve once in Studio).

Thanks for the report — credited.

Sign in to reply.