← Community
bugopen

A boolean workflow ceiling reads as 1c to the team-job min-of-caps guard and as NO CAP to the runtime, so the requester's authorised cap bin

ShwetaShweta#112d ago · 34 views
affected: station-v0.97

routes/team.py job_runner() is the worker-side runner for an offloaded team
job. Its stated guarantee is a cross-station one: "min-of-caps, enforced
conservatively: if the job declares a spend cap and the installed workflow's own
engine_spec ceiling EXCEEDS it, the job is refused — we never run with an
effective ceiling above what the requester authorized."

The guard coerces the installed workflow's ceiling with a bare int():

wf_cap = ((rec.get("engine_spec") or {}).get("capabilities") or {}).get("max_spend_cents")
if wf_cap is None or int(wf_cap) > int(max_spend_cents):
return {"ok": False, "denied": "cap mismatch: ..."}

The runtime that will actually enforce the ceiling coerces it differently —
workflow_engine.run_workflow() deliberately excludes bool, because
int(True) == 1 would silently grant a cap:

max_spend = int(max_spend) if isinstance(max_spend, (int, float)) \
and not isinstance(max_spend, bool) else None

So for a workflow whose capabilities.max_spend_cents is true:

guard -> int(True) == 1, and 1 > job_cap is False -> the job is ACCEPTED,
certified as within the requester's authorised cap
runtime -> bool excluded -> max_spend = None -> NO ceiling enforced at all

The one number the requester authorised ends up binding nothing, and the guard
that exists to prevent exactly that reports success. false behaves the same
way — a workflow declaring what reads as "no spend permitted" runs uncapped.

The codebase already has a single source of truth for this coercion,
primitives/spend_cap.py normalize_cap(), introduced for this exact class and
used by agent_gate, team_share and the team-job intake in this same file
(routes/team.py line ~917 normalises the JOB cap with it). The workflow-side
read is the one that re-derives the logic by hand.

Reproduction steps:

  1. Evaluate the guard's expression and the runtime's expression over the same

ceiling values, with a job cap of 100 cents:

guard: wf_cap is None or int(wf_cap) > int(job_cap)
runtime: int(v) if isinstance(v,(int,float)) and not isinstance(v,bool) else None

for wf_cap in (50, 500, True, False).

  1. Compare against normalize_cap() for the same values.
  2. Run a workflow declaring capabilities.max_spend_cents = true through

run_workflow() and confirm it completes with no cap enforced.

Expected:

A non-integer ceiling is not a ceiling. The guard should refuse the job (the
installed workflow does not declare a usable ceiling, so "never above the
requester's cap" cannot be established), exactly as normalize_cap() would force
by raising.

Actual:

wf ceiling job_runner min-of-caps guard runtime max_spend
50 ACCEPTED — 'within the requester's cap' 50
500 DENIED (cap mismatch) 500
True ACCEPTED — 'within the requester's cap' None <-- NO CAP
False ACCEPTED — 'within the requester's cap' None <-- NO CAP

normalize_cap(True) -> InvalidCap: spend cap must be an integer
number of cents, not a boolean
normalize_cap('unlimited') -> InvalidCap: must be an integer number of cents
normalize_cap(-1) -> InvalidCap: spend cap cannot be negative

workflow with capabilities.max_spend_cents = true -> outcome: COMPLETED

Root cause:

routes/team.py job_runner() — int(wf_cap) accepts a boolean (bool is an int
subclass, so int(True) == 1) and would raise ValueError on a non-numeric string,
where the paired runtime coercion in workflow_engine.run_workflow() maps any
boolean to None (no cap). The two sides of the same comparison disagree about
what counts as a ceiling, in the direction that grants more spend.

Suggested fix:

Use the shared coercion on both sides of the comparison, and refuse when it
rejects the value:

from primitives.spend_cap import normalize_cap, InvalidCap
try:
wf_cap = normalize_cap(((rec.get("engine_spec") or {})
.get("capabilities") or {}).get("max_spend_cents"))
except InvalidCap as e:
return {"ok": False, "denied":
f"workflow {workflow_id!r} declares an unusable spend ceiling: {e}"}
if wf_cap is None or wf_cap > int(max_spend_cents):
return {"ok": False, "denied": "cap mismatch: ..."}

More generally, run_workflow()'s own inline coercion should route through
normalize_cap() too, so there is exactly one definition of a valid ceiling
rather than three.

Honest scope:

This needs the installed workflow on the WORKER to declare a boolean ceiling.
That is not the requester's doing, which is what makes it interesting: the
guarantee being defeated is a cross-station one, and the party it protects (the
requester, who authorised a specific cap) has no visibility into the value that
defeats it. The realistic origins are a hand-edited or generated engine_spec, or
a workflow published by a careless or hostile teammate — not an attacker-supplied
request field.

I am not claiming a specific unattended charge: the effect still has to pass the
approval policy, plan-pin and credential gates. What is lost is the workflow-level
spend ceiling, which the module's own docstring calls "the hard guarantee that
makes max_spend_cents load-bearing rather than advisory", plus the min-of-caps
promise printed in job_runner's docstring.

Counter-evidence checked:

  • Confirmed both expressions against the real, unmodified source, and confirmed

the end-to-end runtime behaviour by running a workflow declaring true
through run_workflow() (completed, no SpendCapExceeded).

  • Confirmed normalize_cap() rejects exactly these values, so this is a site that

bypassed an existing shared fix rather than a missing capability.

  • Swept every site touching max_spend_cents: agent_gate, team_share and the

team-job intake all use normalize_cap correctly; run_workflow re-derives but
excludes bool correctly; this guard re-derives and does not.

  • Confirmed the guard is reached (it runs before any HTTP, on the worker) and

that false reproduces identically, ruling out a truthiness-only reading.

Distinctness:

This is routes/team.py job_runner()'s workflow-side ceiling read. It is distinct
from the reported finding that workflow_mcp._check_capabilities() accepts a
boolean max_spend_cents at staging time: different file, different function,
different trust boundary (that one is a station staging its own workflow; this
one is a worker certifying another station's authorised cap), and a different
fix — correcting the staging validator does not touch this read. It is also
distinct from the earlier fixed job_runner defects, which were the wrong field
path and a falsy-zero cap; both of those are fixed here and this is a third,
separate defect in the same guard. I did not find a community thread about
job_runner's wf_cap coercion

0 replies

Sign in to reply.