← Community
bugopen

The Tier-1 "a require_human plan may run live only when a human is present" gate treats the "cli" session channel as inherently human-presen

ShwetaShweta#17d ago · 15 views
affected: station-v1.5.0

The Tier-1 "a require_human plan may run live only when a human is present" gate treats the "cli" session channel as inherently human-present — but routes/team.py's job_runner() authenticates a fully automated, no-human-present team-relay execution with that exact same channel, silently defeating the gate (v1.5.0)

Reproduction steps:

  1. routes/dispatch_workflow.py's _handle_dag_run() (the handler behind

POST /api/workflow/dag/run, the ONE place a workflow actually executes
live) reads a channel derived from how the request authenticated
(line 580/712, `channel = getattr(handler, "_session_channel", None)
or "session"`), and gates require_human plans on it:
# · a require_human plan may run live only when a HUMAN is present to
# approve it — an interactive session/cli channel (someone clicked
# Run in Studio or the CLI). An unattended channel (scheduler) or
# an autonomous one (mcp) cannot self-approve, so it is refused
# here rather than firing effects nobody reviewed.
if _breq == "require_human" and channel not in ("session", "cli"):
_pin_rec = _pp.load(WS, wf_id) or {}
_pin_human = _pp.human_approved(_pin_rec)
if not (channel == "scheduler" and _pin_human):
return _refused({...}, gate="require_human_unattended")
The whole block is SKIPPED whenever channel == "cli" — no
human_approved() check, no pin inspection, nothing. The design
assumption, stated in the comment, is that "cli" means "someone clicked
Run in Studio or the CLI" — an actual human, right now, at a keyboard.

  1. studio_server.py's _require_session() sets _session_channel = "cli"

whenever the request's X-RailCall-Session header matches the contents
of WS/cli_session_token (a 0600, same-user file, generated once at
station boot for the operator's own railcall CLI to authenticate
with).

  1. routes/team.py's job_runner() — the function team_jobs.

handle_job_offer() calls to actually EXECUTE a job a REMOTE team member
offered over the mesh, with nobody at this machine's keyboard —
authenticates its own outbound loopback call to that same
/api/workflow/dag/run using that exact same cli_session_token:
with open(os.path.join(WS, "cli_session_token"), encoding="utf-8") as f:
tok = f.read().strip()
req = urllib.request.Request(
f"http://127.0.0.1:{port}/api/workflow/dag/run",
data=json.dumps({"id": workflow_id, "dry_run": bool(dry_run),
"context": context or {}, "invoker": "relay", ...}).encode(),
headers={"Content-Type": "application/json",
"X-RailCall-Session": tok},
method="POST")
The request body's own "invoker": "relay" field is cosmetic — the
comment right above it even says "server-side channel truth still
wins" — but the server-side "truth" is derived purely from WHICH TOKEN
authenticated the request, and job_runner presents the identical token
a genuine interactive CLI invocation would. _require_session() cannot
distinguish "the operator just typed railcall run wf_move_money" from
"a team-relay job fired this with zero humans involved" — both get
_session_channel = "cli".

  1. Run repro_cli_channel_require_human_bypass_v150.py against a clean

v1.5.0 extraction. It plans a real workflow whose only node is an
effect that trips the hard require_human floor (a $5000 Stripe charge),
pins it with an UNSEALED "migration:*" approval (proven via
plan_pin.human_approved() returning False — i.e. no human ever actually
approved this blast radius), and drives the REAL, unmodified
_handle_dag_run() twice with identical everything except the session
channel:
plan pinned. blast_radius.requires: require_human
human_approved(pin) for this pin: False (unsealed migration pin)

=== channel='scheduler' (unattended daemon, no sealed approval) ===
reached run_workflow: False
response: {'ok': False, 'error': "this workflow requires a human to
approve its blast radius; an unattended run may not self-approve
it...", 'requires': 'require_human', 'channel': 'scheduler', ...}

=== channel='cli' (what job_runner() authenticates its automated
execution as) ===
reached run_workflow: True
response: {'ok': True, 'mode': 'run', ..., 'invoker': 'cli',
'outcome': 'ROLLED_BACK', ...,
'error': "StripeAuthError('LIVE_ENVIRONMENT=True but
STRIPE_SECRET_KEY not set in env...')", ...}
CONFIRMED
run_workflow() is not stubbed — the "cli" case reaches and attempts the
REAL live effect call, failing only because this sandbox has no
STRIPE_SECRET_KEY configured, not because of any authorization gate.

Expected:
"cli" should mean what the comment says it means — a human physically
issuing the command right now — or the gate should stop trusting it as a
proxy for that. Either way, an automated, no-human-present execution path
must be refused a require_human plan exactly as the scheduler channel is,
not treated as strictly MORE trusted than scheduler (which at least
requires a cryptographically sealed human_approved() pin).

Actual:
Any code path that can present the cli_session_token — currently exactly
one: routes/team.py's job_runner(), invoked with zero human involvement
whenever a team-relay job offload fires — bypasses the require_human gate
entirely, with no human-presence check of any kind, weaker than even the
scheduler channel. Combined with finding #2 in this file (a role-addressed
live job_offer executes independently on every worker in the mesh with no
sender-side restriction enforced on the receiving end), this chains into:
any current team member holding only the operator role can mint a single
job_offer naming an installed require_human workflow, and it executes
live, unattended, and unapproved — with real money or a real message
moving — on every worker station in the team, which is exactly the
scenario this gate's own comments describe existing to prevent.

Suggested fix:
Give job_runner() its own dedicated channel identity instead of reusing
cli_session_token — e.g. a separate WS/team_relay_session_token,
authenticated the same way but mapped to _session_channel = "team_relay"
in _require_session() — and require the SAME sealed human_approved() proof
for it that the scheduler channel already requires:
if _breq == "require_human" and channel not in ("session",):
_pin_rec = _pp.load(WS, wf_id) or {}
_pin_human = _pp.human_approved(_pin_rec)
if not (channel in ("scheduler", "team_relay", "cli") and _pin_human):
return _refused(...)
i.e. treat EVERY non-interactive channel (scheduler, team_relay, and
arguably cli itself, since a script or another program can present that
same file-backed token just as easily as the real CLI) as needing the
cryptographic seal, and reserve unconditional trust for "session" (the
browser-templated, per-boot in-memory SESSION_TOKEN) alone, which is the
only one of these that actually requires a human to have loaded a page
this station itself served.

0 replies

Sign in to reply.