gateway: phase-6 cap walk (PIPELINE 2) behind enforce_budgets - #1684
Merged
Merged
Conversation
CheckCaps reads the accumulators PostLLMHook writes and rejects with 402 when a verified macaroon is over any spend cap: - run_cost_exceeded / run_step_exceeded: every chain layer, leaf first (a parent's exhausted cap stops its children) - realm_budget_exceeded (phase 11) / ua_budget_exceeded: cost:ua - agent_cost_exceeded: agent_budgets bucket; cap_usd 0 blocks outright - budget_check_unavailable on Redis errors (fail closed) Gated by a new enforce_budgets flag (config key or BIFROST_PLUGIN_ENFORCE_BUDGETS), effective only alongside enforce_macaroons. Off (default), over-cap calls log 'auth: budget shadow …' and pass through with claims stamped so accounting keeps running. The accumulator now writes cost:ua when the UA carries a realm cap for this swarm's realm, not only an org-wide max_total_usd — the cap walk reads the same counter for both.
Evanfeenstra
force-pushed
the
gateway-cap-walk
branch
from
September 14, 2026 19:22
96f007d to
1e18dfc
Compare
Evanfeenstra
added a commit
that referenced
this pull request
Sep 14, 2026
) Operator half of the phase-6 kill switches (#1683, #1684) in the admin SPA. Scope is kill/unkill + live hot state only; no config editing, no bulk actions, no analytics changes. - api/types.ts: RunStateResponse, AgentStateResponse, KillRunResponse, KillAgentResponse mirroring hotstate.go (hand-maintained; no tygo). - api/queries.ts: useRunState (2s in flight / 30s done / 500ms for 30s after a kill), useAgentState (10s), useAgentStates (30s per row), useKillRun / useUnkillRun / useKillAgent / useUnkillAgent. 503 (no Redis) folds into data === null with a 60s retry. - components/KillConfirmModal: one modal, two modes — plain confirm for runs, typed agent name for agents. Explains scope, TTL (1h / 24h) and "takes effect on the next LLM call; enforced only with enforce_macaroons=true". No window.confirm (Hive iframe sandbox). - components/StatusBadge: running / killed / exceeded / done with the derivation documented in one place. - RunDetail: live-state card (cost, steps, last tools, kill flag, "state expires in") + Kill / Unkill; 503 renders as an inline note with the switch disabled. - AgentDetail: killed / exceeded badge in the header + Kill / Unkill agent. Agents: kill-state column read from /state per row. - icons: StopIcon. - vite.config: bypass /_plugin/ui/* in the dev proxy so Vite serves the SPA instead of the gateway's embedded bundle (local edits never showed otherwise); GATEWAY_URL override for the proxy target. - .claude/launch.json: gateway-ui dev-server entry. dist/ stays the tracked placeholder — the repo doesn't commit builds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1683 (kill switches) — base is
gateway-kill-switches; retarget tomainonce that merges.What
CheckCaps(internal/auth/capwalk.go) is phase-6 PIPELINE 2: one pipelined Redis read of the accumulators PostLLMHook already writes, then a fixed-order comparison:cost:run:<r>per chain layer, leaf firstmax_cost_usdrun_cost_exceededcost:ua:<nonce>realm_budgets[<this realm>].max_total_usd(phase 11)realm_budget_exceededcost:ua:<nonce>ua.budget.max_total_usdua_budget_exceededsteps:run:<r>per chain layermax_stepsrun_step_exceededcost:agent:<a>:<bucket>agent_budgetscapagent_cost_exceeded>=comparisons on past spend; one call of overshoot is accepted per the plan.agent_budgetsentry withcap_usd: 0blocks the agent outright (the plan's "permanent kill").budget_check_unavailable(fail closed); Redis unconfigured ⇒ no-op.Gating
New
enforce_budgetsflag (config key orBIFROST_PLUGIN_ENFORCE_BUDGETS, same grammar/source=reporting as #1674). Effective only withenforce_macaroons=true— otherwise budgets are bypassable by dropping the macaroon, so it logs a warning and stays in shadow.Shadow (default): over-cap calls log
auth: budget shadow code=run_cost_exceeded … detail="run r_x spent $5.0100 of its $5.00 cap"and pass through with claims stamped, so the accumulator keeps counting. That line is what to grep before flipping the flag.
Boot line now reports both flags plus the effective value.
Accumulator change
cost:uais now also written when the UA carries arealm_budgetscap for this swarm's realm (not only an org-widemax_total_usd) — the cap walk reads the same counter for both.Not in this PR
Tool-loop detection,
hard_ceiling,user_id == customer_idcross-check,/_plugin/config/*overrides. Sphinx-swarm plumbing forBIFROST_PLUGIN_ENFORCE_BUDGETS(like swarm#757 did for macaroons).Tests
21 new miniredis tests: each rejection code, ancestor-caps-child, realm vs UA precedence, realm-cap-only UA read, zero-cap block, zero caveats = uncapped, order, fail-closed, namespace; plus
Evaluate/ApplyToLLMPrein enforce / shadow / budgets-without-macaroons; config + env override; accumulator realm-cap write.