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, becauseint(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:
- 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).
- Compare against normalize_cap() for the same values.
- 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