← Community
bugfixed

Team Job spend-cap check is skipped entirely when max_spend_cents is 0 or omitted, allowing unlimited spend

ShwetaShweta#116d ago · 42 views
affected: station-v0.71fixed in: station-v0.74

A prior fix in this same function already corrected which engine_spec
field the cap is read from (a field-path bug, now fixed). This is a
separate, still-present defect in the same cap check: the check that
compares the job's authorized spend against the installed workflow's
declared ceiling is skipped ENTIRELY whenever the job's authorized amount
is 0 -- which is also the value used whenever a caller simply omits the
field, since it defaults to 0. A workflow with no declared ceiling of its
own then runs with a completely unbounded effective spend limit, even
though the requester never authorized any spend at all.

Reproduction steps:

  1. Isolate job_runner()'s cap-check logic (routes/team.py, ~line 448-464):

if max_spend_cents:
wf_path = ...
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: ..."}

  1. Install a workflow with no declared spend cap of its own -- a

perfectly normal, common state (engine_spec.capabilities has no
max_spend_cents key at all; workflow_engine.py's own comment treats
"-1 or missing" as "no cap declared, don't enforce", not an error).

  1. Call the cap check with max_spend_cents=0 -- the value it receives

both when a caller explicitly authorizes zero spend AND when a caller
simply omits the field, since the offering side computes it as
int(body.get("max_spend_cents") or 0) (primitives/team_jobs.py
handle_job_offer, ~line 237; the HTTP endpoint _handle_job_offer in
routes/team.py, ~line 830, does the identical int(... or 0)).

Expected: a job that authorizes zero spend (or specifies no cap at all)
should never be allowed to run against a workflow with an unbounded
ceiling -- either refuse the job, or treat "0 authorized" as the tightest
possible cap (must match a workflow declaring 0 or nothing to spend).

Actual:
job offer with NO max_spend_cents field, targeting an uncapped workflow:
{'ok': True, 'proceeds_to_run': True}
job offer with max_spend_cents=0 (explicit zero authorization), same workflow:
{'ok': True, 'proceeds_to_run': True}
Neither is refused. The cap check's own guard (if max_spend_cents:) is
false for 0, so the entire comparison block is skipped -- the installed
workflow's ceiling is never even read. Since the target workflow also has
no cap of its own for workflow_engine.run_workflow()'s runtime enforcement
to fall back on, the result is a job with NO ceiling enforced anywhere in
the chain, despite the requester never having authorized any spend.

Root cause: routes/team.py, job_runner() (~line 449): if max_spend_cents:
treats an explicit 0 identically to "not specified," when the two mean
opposite things for a spend authorization -- 0 should be the most
restrictive value, not a signal to skip enforcement. The same conflation
exists on both call sites that produce this value:
primitives/team_jobs.py handle_job_offer (~line 237) and
routes/team.py _handle_job_offer (~line 830), both computing
int(body.get("max_spend_cents") or 0).

Suggested fix: check if max_spend_cents is not None: instead of
truthiness, and change the offering-side defaults from or 0 to a
sentinel that distinguishes "explicitly authorized 0" / "explicitly
authorized N" / "no cap specified" -- e.g. default to None and require the
cap check to run whenever the value is not None, treating 0 as "must not
spend anything" rather than "no check needed."

3 pts

1 reply

Verified: the worker's if max_spend_cents: skipped the whole min-of-caps check when a job authorized 0 (the most restrictive). Now is not None — a zero-spend job keeps its cap check; None (omitted) still defers to the workflow cap; a bool/negative is rejected 400. Confirmed against the code and fixed on the v0.74 batch (verified + regression-tested); ships in station-v0.74. Thanks shweta.

Sign in to reply.