← Community
bugfixed

Plan re-approval route accepts the scheduler and CLI channels despite its own human-only contract

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

station-v0.83 adds POST /api/workflow/plan/reapprove, which re-pins a live-run
approval after a workflow's plan has changed. It is the counterpart to
plan_pin's "this workflow has CHANGED since it was approved for live runs"
refusal, and it is therefore the single most consequential human act in the
workflow-approval model: it blesses a new blast radius.

The handler's own docstring states the rule:

"Session + human only: an automated/MCP channel must never be able to
bless a workflow whose blast radius moved."

The code enforces only part of it:

channel = getattr(handler, "_session_channel", None) or "session"
if channel == "mcp":
return handler._send(200, {"ok": False, "error": ...})

_require_session() authenticates FOUR channels -- session, mcp, cli and
scheduler -- each from its own dedicated 0600 token file. Only mcp is refused
here, so the scheduler channel passes, and so does cli.

The scheduler channel is the companion daemon's credential and is the
least-trusted of the four by explicit design. studio_server._require_session()
documents its scope:

"The token authenticates /api/schedules/tick and nothing more:
routes/schedules._require_human refuses schedule creation on this channel,
so a daemon credential can never mint the standing grants it exists to
execute."

With this route, that credential can now bless a changed plan -- which is what
authorises the live run the daemon then executes. The loop the separation
exists to prevent (the unattended component approving its own standing grant)
is closed.

The recorded approval also misattributes the actor. plan_pin.pin() is called
with approved_by = "studio_reapprove:<operator name>" unconditionally, so a
re-approval made on the scheduler or cli channel is written into the pin and
the audit_log as though an operator did it in Studio.

The correct pattern already exists in the codebase and is applied to the same
class of act elsewhere: routes/schedules.py's _require_human() refuses the
scheduler channel for schedule create/enable/delete, and two other handlers in
this same dispatch_workflow.py file check
getattr(handler, "_session_channel", None) == "scheduler" explicitly to
refuse humans-only actions. This new route simply uses a narrower check than
its siblings.

Reproduction steps:

  1. Extract a clean station-v0.83 tarball. Put a workflow with an engine_spec at

WS/workflows/<id>.json.

  1. Call _handle_reapprove_plan with a caller whose _session_channel is

"scheduler" and body {"id": "<id>"} -- note it returns a plan_root for
review rather than refusing.

  1. Call it again on the same channel with {"id": "<id>", "reviewed_plan_root":

<that root>}.

  1. Repeat both steps with _session_channel set to "cli", and to "mcp".
  2. For contrast, call routes/schedules.py's _handle_create with the same

scheduler-channel caller.

Expected: steps 2-3 are refused on the scheduler channel exactly as step 5 is,
since both are "alter a standing grant of unattended execution" acts and the
handler's docstring says an automated channel must never bless a moved blast
radius.

Actual: the scheduler channel completes the re-approval and plan_pin records
the new plan with approved_by "studio_reapprove:<operator name>". The cli
channel does the same. Only mcp is refused. Step 5 correctly returns HTTP 403,
"the scheduler channel may not create or modify schedules".

Root cause: the channel check tests equality against the single value "mcp"
rather than asserting the channel is a human one. Any channel the station adds
or already has -- cli and scheduler today -- falls through the gate.

Suggested fix: invert the test so it allows rather than denies, e.g. refuse
unless channel is "session", which is the only genuinely human-in-Studio
channel; or reuse the existing helper shape from routes/schedules.py so the two
surfaces cannot drift apart again. Separately, derive approved_by from the
authenticated channel rather than hardcoding the studio_reapprove prefix, so
the pin and the audit entry name the credential that actually approved.

5 pts

1 reply

Fixed in station-v0.86. Great find — /api/workflow/plan/reapprove was accepting the scheduler and CLI channels, so an unattended channel could bless a changed blast radius. Re-approval is now session-only: only an interactive Studio human can re-approve; scheduler / cli / mcp are refused. Credited.

Sign in to reply.