atlas-iac/testing/tests/test_hermes_hux_policy_approvals.py

412 lines
25 KiB
Python

"""HUX-05 approval queue and the pre-side-effect gate.
Security obligations exercised: SO-35 (only a human surface with router or
relay trust decides; ``decision.by`` is the asserted user), SO-36 (``once``
is consumed exactly once), SO-37 (the gate must present the argument hash
recorded at request time), SO-39 (external side effects always need an
approval record), SO-40 (an exhausted budget blocks new approvals), SO-42
(pending approvals expire 24 h after ``requested_at``).
"""
from __future__ import annotations
import importlib
import json
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
FOUNDATION = ROOT / "dockerfiles" / "hermes-hux-foundation"
if str(FOUNDATION) not in sys.path:
sys.path.insert(0, str(FOUNDATION))
import hux # noqa: E402
from hux import audit, budgets, contracts, identity, policy, store # noqa: E402
from hux import events as hux_events # noqa: E402
from hux.server import build_router # noqa: E402
SCHEMAS = contracts.load_all()
HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
OTHER = {**HEADERS, "X-Hux-Subject": "usr_fedcba9876543210"}
WORKER = {**HEADERS, "X-Hux-Surface": "worker", "X-Hux-Trust": "worker", "X-Hux-Relay-Key": "wk"}
RELAY = {**HEADERS, "X-Hux-Surface": "telegram", "X-Hux-Trust": "relay", "X-Hux-Relay-Key": "rk"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
T0 = datetime(2026, 8, 24, 12, 0, tzinfo=timezone.utc)
CONV = "conv_0001abcd"
HASH = "sha256:" + "ab" * 32
OTHER_HASH = "sha256:" + "cd" * 32
@pytest.fixture
def router(tmp_path):
instance = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk", "HUX_RELAY_KEY": "rk"})
response = instance.dispatch(
"POST", "/hux/v1/runs/run_9f/budget", WORKER, json.dumps({"conversation_id": CONV}).encode()
)
assert response.status == 200
return instance
@pytest.fixture
def frozen(monkeypatch):
state = {"now": T0}
monkeypatch.setattr(policy, "clock", lambda: state["now"])
return state
@pytest.fixture
def events(monkeypatch):
"""Stand-in for the events lane; records every emit call."""
seen: list[dict] = []
monkeypatch.setattr(hux_events, "emit", lambda store, identity, conversation_id, kind, summary, **extra: seen.append({"conversation_id": conversation_id, "kind": kind, "summary": summary, **extra}))
return seen
def call(router, method, path, body=None, headers=HEADERS):
raw = b"" if body is None else json.dumps(body).encode()
response = router.dispatch(method, path, headers, raw)
return response.status, response.body
def valid(record):
problems = contracts.validate_record(record, SCHEMAS)
assert problems == [], problems
return record
def request_body(capability="write_files", external=False, run_id="run_9f", **extra):
evidence = [{"kind": "tool_call", "id": "call-7", "hash": HASH}]
return {"run_id": run_id, "conversation_id": CONV, "capability": capability,
"request": {"summary": f"do {capability}", "risk": "low", "external": external, "evidence": evidence, **extra}}
def set_autonomy(router, level):
status, body = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": level})
assert status == 200, body
# --- creation resolves against the matrix ------------------------------------------
def test_safe_policy_auto_allows_reads_and_queues_mutations(router, frozen, events):
status, body = call(router, "POST", "/hux/v1/approvals", request_body("read_files"), WORKER)
assert status == 201 and valid(body)["status"] == "approved"
assert body["decision"] == {"choice": "once", "by": {"type": "system", "id": "policy"}, "at": policy.iso(T0)}
status, body = call(router, "POST", "/hux/v1/approvals", request_body("write_files"), WORKER)
assert status == 201 and valid(body)["status"] == "pending" and "decision" not in body
assert body["expires_at"] == policy.iso(T0 + timedelta(hours=24))
status, body = call(router, "POST", "/hux/v1/approvals", request_body("network"), WORKER)
assert status == 201 and valid(body)["status"] == "denied" and body["decision"]["choice"] == "deny"
assert [e["kind"] for e in events] == ["approval.resolved", "approval.requested", "approval.resolved"]
assert events[0]["evidence"] == [{"kind": "approval", "id": events[0]["evidence"][0]["id"]}]
def test_external_side_effects_always_need_a_human_regardless_of_autonomy(router, events):
set_autonomy(router, "autonomous")
status, body = call(router, "POST", "/hux/v1/approvals", request_body("send_message"), WORKER)
assert status == 201 and body["status"] == "approved"
status, body = call(router, "POST", "/hux/v1/approvals", request_body("send_message", external=True), WORKER)
assert status == 201 and body["status"] == "pending"
for always_ask in ("deploy", "external_side_effect"):
status, body = call(router, "POST", "/hux/v1/approvals", request_body(always_ask), WORKER)
assert body["status"] == "pending", always_ask
call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "autonomous", "grants": [{"capability": "network", "decision": "deny"}]})
status, body = call(router, "POST", "/hux/v1/approvals", request_body("network", external=True), WORKER)
assert body["status"] == "denied", "deny beats external ask"
def test_idempotency_key_returns_the_original(router):
key = {"Idempotency-Key": "run_9f:approval:call-7"}
status, first = call(router, "POST", "/hux/v1/approvals", request_body(), {**WORKER, **key})
assert status == 201 and first["idempotency_key"] == "run_9f:approval:call-7"
status, again = call(router, "POST", "/hux/v1/approvals", request_body(), {**WORKER, **key})
assert (status, again) == (200, first)
status, conflict = call(router, "POST", "/hux/v1/approvals", request_body("shell"), {**WORKER, **key})
assert (status, conflict["code"]) == (409, "conflict")
status, other = call(router, "POST", "/hux/v1/approvals", request_body(), {**WORKER, "Idempotency-Key": "run_9f:approval:call-8"})
assert status == 201 and other["id"] != first["id"]
assert call(router, "GET", f"/hux/v1/approvals/{first['id']}")[1] == first
@pytest.mark.parametrize("body,status", [
(None, 400), ([], 400), ({"conversation_id": "nope", "capability": "shell"}, 400),
({"conversation_id": CONV, "capability": "teleport"}, 400),
({"conversation_id": CONV, "capability": "shell", "run_id": "r"}, 400),
({"conversation_id": CONV, "capability": "shell", "run_id": "r", "request": {"summary": "", "risk": "low"}}, 400),
({"conversation_id": CONV, "capability": "shell", "run_id": "r", "request": {"summary": "x", "risk": "silly"}}, 400),
])
def test_create_rejects_bad_bodies(router, body, status):
got, error = call(router, "POST", "/hux/v1/approvals", body, WORKER)
assert got == status and valid(error)["code"] == "invalid"
# --- deciding -----------------------------------------------------------------------
def pending(router, capability="write_files", **kw):
body = request_body(capability, **kw)
run_id = body["run_id"]
if budgets.run_conversation(router_store(router), run_id) is None:
assert call(router, "POST", f"/hux/v1/runs/{run_id}/budget", {"conversation_id": body["conversation_id"]}, WORKER)[0] == 200
status, body = call(router, "POST", "/hux/v1/approvals", body, WORKER)
assert status == 201 and body["status"] == "pending", body
return body
def test_only_humans_on_router_or_relay_decide(router, tmp_path):
record = pending(router)
status, error = call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"}, WORKER)
assert (status, error["code"]) == (403, "forbidden")
api = {**HEADERS, "X-Hux-Surface": "api"}
assert call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"}, api)[0] == 403
status, body = call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"}, RELAY)
assert status == 200 and valid(body)["status"] == "approved"
assert body["decision"]["by"] == {"type": "user", "id": HEADERS["X-Hux-Subject"]}
rows = audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})))
assert [(r["action"], r["outcome"]) for r in rows if r["action"] == "approvals.decide"] == [("approvals.decide", "deny")] * 2 + [("approvals.decide", "allow")]
def test_terminal_states_never_change_again(router, events):
record = pending(router)
path = f"/hux/v1/approvals/{record['id']}"
assert call(router, "POST", path, {"choice": "maybe"})[0] == 400
assert call(router, "POST", path, [])[0] == 400
status, body = call(router, "POST", path, {"choice": "deny"})
assert status == 200 and body["status"] == "denied" and body["decision"]["choice"] == "deny"
status, error = call(router, "POST", path, {"choice": "once"})
assert (status, error["code"]) == (409, "conflict")
assert call(router, "GET", path)[1] == body
assert events[-1]["kind"] == "approval.resolved" and "denied" in events[-1]["summary"]
def test_pending_approvals_expire_after_24h(router, frozen):
record = pending(router)
frozen["now"] = T0 + timedelta(hours=24)
status, body = call(router, "GET", f"/hux/v1/approvals/{record['id']}")
assert status == 200 and valid(body)["status"] == "expired"
assert call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"})[0] == 409
assert call(router, "GET", "/hux/v1/approvals?status=expired")[1]["items"] == [body]
assert call(router, "GET", "/hux/v1/approvals?status=pending")[1]["items"] == []
def test_session_and_always_create_grants_at_their_scope(router, frozen):
record = pending(router, "shell")
status, body = call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "session"})
assert status == 200 and body["decision"]["choice"] == "session"
conv_policy = call(router, "GET", f"/hux/v1/policy?scope=conversation&scope_id={CONV}")[1]
assert valid(conv_policy)["scope"] == {"level": "conversation", "scope_id": CONV}
assert conv_policy["grants"] == [{"capability": "shell", "decision": "allow", "expires_at": policy.iso(T0 + policy.SESSION_TTL), "granted_by": {"type": "user", "id": HEADERS["X-Hux-Subject"]}}]
status, body = call(router, "POST", "/hux/v1/approvals", request_body("shell"), WORKER)
assert body["status"] == "approved", "later shell requests in this conversation auto-approve"
assert call(router, "POST", "/hux/v1/approvals", request_body("write_files"), WORKER)[1]["status"] == "pending"
record = pending(router, "write_files")
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "always"})
global_policy = call(router, "GET", "/hux/v1/policy")[1]
assert [g["capability"] for g in global_policy["grants"]] == ["write_files"]
assert global_policy["grants"][0]["expires_at"] == policy.iso(T0 + policy.ALWAYS_TTL)
frozen["now"] = T0 + timedelta(hours=25)
record = pending(router, "shell", run_id="run_other")
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "session"})
conv_policy = call(router, "GET", f"/hux/v1/policy?scope=conversation&scope_id={CONV}")[1]
assert len(conv_policy["grants"]) == 1 and conv_policy["revision"] == 2, "a repeat grant replaces, never duplicates"
def test_list_filters_and_orders_by_request_time(router, frozen):
first = pending(router, "shell")
frozen["now"] = T0 + timedelta(seconds=5)
second = pending(router, "write_files")
status, body = call(router, "GET", "/hux/v1/approvals")
assert status == 200 and [i["id"] for i in body["items"]] == [first["id"], second["id"]] and body["next"] is None
assert call(router, "GET", "/hux/v1/approvals?status=pending")[1]["items"] == body["items"]
assert call(router, "GET", "/hux/v1/approvals?status=approved")[1]["items"] == []
assert call(router, "GET", "/hux/v1/approvals?status=bogus")[0] == 400
for item in body["items"]:
valid(item)
def test_second_subject_cannot_see_or_decide(router):
record = pending(router)
assert call(router, "GET", f"/hux/v1/approvals/{record['id']}", headers=OTHER)[0] == 404
assert call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"}, OTHER)[0] == 404
assert call(router, "GET", "/hux/v1/approvals", headers=OTHER)[1]["items"] == []
assert call(router, "GET", "/hux/v1/approvals/apr_doesnotexist")[0] == 404
assert call(router, "GET", "/hux/v1/approvals/..")[0] == 400
# --- budgets block new approvals -------------------------------------------------------
def test_exhausted_budget_blocks_new_approvals(router, events):
call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "safe", "budgets": {"tool_calls_per_run": 2}})
status, body = call(router, "POST", "/hux/v1/runs/run_9f/budget", {"conversation_id": CONV, "tool_calls": 2}, WORKER)
assert status == 200 and valid(body)["exhausted"] == ["tool_calls_per_run"]
status, error = call(router, "POST", "/hux/v1/approvals", request_body("read_files"), WORKER)
assert (status, error["code"]) == (429, "budget_exhausted") and error["details"] == ["tool_calls_per_run"]
assert [e["kind"] for e in events] == ["budget.exhausted", "budget.exhausted"]
status, body = call(router, "POST", "/hux/v1/approvals", request_body("read_files", run_id="run_fresh"), WORKER)
assert (status, body["code"]) == (400, "invalid"), "an unbound run cannot create an approval record"
call(router, "POST", "/hux/v1/runs/run_fresh/budget", {"conversation_id": CONV}, WORKER)
status, body = call(router, "POST", "/hux/v1/approvals", request_body("read_files", run_id="run_fresh"), WORKER)
assert (status, body["code"]) == (429, "budget_exhausted"), "run rotation cannot reset conversation spend"
# --- gate --------------------------------------------------------------------------
def gate(router, run_id="run_9f", capability="write_files", argument_hash=HASH, external=False, **extra):
return call(router, "POST", f"/hux/v1/runs/{run_id}/gate", {"capability": capability, "argument_hash": argument_hash, "external": external, **extra}, WORKER)
def test_gate_blocks_before_approval_and_releases_once_exactly_once(router, events, tmp_path):
status, body = gate(router)
assert status == 200 and body == {"proceed": False, "reason": "no approval for this run and capability"}
record = pending(router)
assert gate(router)[1]["proceed"] is False and "pending" in gate(router)[1]["reason"]
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"})
status, wrong = gate(router, argument_hash=OTHER_HASH)
assert wrong["proceed"] is False and "different arguments" in wrong["reason"], "TOCTOU (SO-37)"
status, body = gate(router)
assert body == {"proceed": True, "approval_id": record["id"], "reason": "released"}
status, again = gate(router)
assert again["proceed"] is False and "already consumed" in again["reason"], "once is once (SO-36)"
kinds = [e["kind"] for e in events]
assert kinds.count("side_effect.released") == 1 and kinds.count("side_effect.blocked") == 5
released = next(e for e in events if e["kind"] == "side_effect.released")
assert released["evidence"] == [{"kind": "approval", "id": record["id"]}] and released["run_id"] == "run_9f"
served = call(router, "GET", f"/hux/v1/approvals/{record['id']}")[1]
assert valid(served) and "_consumed_at" not in served
rows = audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})))
assert [r["outcome"] for r in rows if r["action"] == "gate.check"] == ["deny", "deny", "deny", "deny", "allow", "deny"]
def test_gate_session_is_reusable_within_the_conversation(router):
record = pending(router, "shell")
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "session"})
assert gate(router, capability="shell")[1]["proceed"] is True
assert gate(router, capability="shell", argument_hash=OTHER_HASH)[1]["proceed"] is True
assert gate(router, run_id="run_later", capability="shell")[1]["proceed"] is False, "unknown runs inherit no approval"
status, unbound = call(router, "POST", "/hux/v1/approvals", request_body("shell", run_id="run_unbound"), WORKER)
assert (status, unbound["code"]) == (400, "invalid"), "an unknown run cannot mint any approval record"
call(router, "POST", "/hux/v1/runs/run_later/budget", {"conversation_id": CONV}, WORKER)
assert gate(router, run_id="run_later", capability="shell")[1]["proceed"] is False, "the old approval record never spans runs"
status, fresh = call(router, "POST", "/hux/v1/approvals", request_body("shell", run_id="run_later"), WORKER)
assert status == 201 and fresh["status"] == "approved" and fresh["id"] != record["id"]
assert gate(router, run_id="run_later", capability="shell")[1]["approval_id"] == fresh["id"]
call(router, "POST", "/hux/v1/runs/run_elsewhere/budget", {"conversation_id": "conv_elsewhere01", "tokens": 1}, WORKER)
status, body = gate(router, run_id="run_elsewhere", capability="shell", conversation_id=CONV)
assert body["proceed"] is False, "the run's own conversation wins over the body's claim (F4)"
elsewhere = request_body("shell", run_id="run_elsewhere")
elsewhere["conversation_id"] = "conv_elsewhere01"
status, scoped = call(router, "POST", "/hux/v1/approvals", elsewhere, WORKER)
assert status == 201 and scoped["status"] == "pending", "a session grant never crosses conversations"
assert gate(router, capability="write_files")[1]["proceed"] is False
def test_gate_requires_external_approvals_for_external_effects_and_respects_expiry(router, frozen):
record = pending(router, "send_message")
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"})
status, body = gate(router, capability="send_message", external=True)
assert body["proceed"] is False and "not requested as external" in body["reason"]
record = pending(router, "send_message", external=True)
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"})
frozen["now"] = T0 + timedelta(hours=25)
status, body = gate(router, capability="send_message", external=True)
assert body["proceed"] is False and "expired" in body["reason"]
def test_gate_rejects_bad_bodies_and_other_tenants(router):
assert gate(router, capability="teleport")[0] == 400
assert gate(router, argument_hash="md5:zz")[0] == 400
assert gate(router, conversation_id="nope")[0] == 200, "the body's conversation_id is ignored, never validated (F4)"
assert call(router, "POST", "/hux/v1/runs/run_9f/gate", [], WORKER)[0] == 400
assert call(router, "POST", f"/hux/v1/runs/{'r' * 121}/gate", {"capability": "shell", "argument_hash": HASH}, WORKER)[0] == 400
record = pending(router)
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"})
status, body = call(router, "POST", "/hux/v1/runs/run_9f/gate", {"capability": "write_files", "argument_hash": HASH}, OTHER)
assert (status, body["code"]) == (403, "forbidden")
def test_events_module_absence_is_tolerated(router, monkeypatch):
monkeypatch.setitem(sys.modules, "hux.events", None)
monkeypatch.delattr(hux, "events")
with pytest.raises(ImportError):
importlib.import_module("hux.events")
assert call(router, "POST", "/hux/v1/approvals", request_body("read_files"), WORKER)[0] == 201
policy.emit(None, None, None, "x", "no conversation means no event")
assert budgets.hashes_of({"request": {}}) == set()
# --- F4 / F6: external effects, run conversation and budgets at the gate --------------
def test_external_effects_release_only_for_the_same_run_and_arguments(router):
"""F4 (high) / SO-39: a session or always approval for an external effect never releases another run or other arguments."""
record = pending(router, "send_message", external=True)
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "session"})
assert gate(router, capability="send_message", external=True)[1]["proceed"] is True
assert gate(router, capability="send_message", external=True)[1]["proceed"] is True, "session is reusable for the same run and hash"
status, body = gate(router, capability="send_message", external=True, argument_hash=OTHER_HASH)
assert body["proceed"] is False and "different arguments" in body["reason"]
status, body = gate(router, run_id="run_z", capability="send_message", external=True, conversation_id=CONV)
assert body["proceed"] is False and body["reason"] == "no approval for this run and capability"
assert gate(router, run_id="run_z", capability="send_message", external=False)[1]["proceed"] is False, "an external approval is not a general one"
always = pending(router, "send_message", external=True, run_id="run_y")
call(router, "POST", f"/hux/v1/approvals/{always['id']}", {"choice": "always"})
assert gate(router, run_id="run_y", capability="send_message", external=True)[1]["proceed"] is True
assert gate(router, run_id="run_w", capability="send_message", external=True)[1]["proceed"] is False, "always never spans runs for external effects"
def test_once_approvals_name_exactly_one_tool_call(router):
"""F4 / SO-37: a request carrying two tool_call hashes is refused so a once approval designates one hash."""
body = request_body("shell")
body["request"]["evidence"] = [{"kind": "tool_call", "id": "a", "hash": HASH}, {"kind": "tool_call", "id": "b", "hash": OTHER_HASH}]
status, error = call(router, "POST", "/hux/v1/approvals", body, WORKER)
assert (status, error["code"]) == (400, "invalid") and "exactly one tool_call" in error["message"]
body["request"]["evidence"] = [{"kind": "tool_call", "id": "a", "hash": HASH}, {"kind": "file", "id": "notes.md"}]
assert call(router, "POST", "/hux/v1/approvals", body, WORKER)[0] == 201, "other evidence kinds do not count"
body["request"]["evidence"] = "not-a-list"
assert call(router, "POST", "/hux/v1/approvals", body, WORKER)[0] == 400, "the contract still rejects it, without a 500"
def test_gate_refuses_when_the_run_budget_is_exhausted(router, events):
"""F6 (medium) / SO-40: an approved effect still does not proceed once any limit is spent."""
record = pending(router, "shell")
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "session"})
assert gate(router, capability="shell")[1]["proceed"] is True
call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "conversation", "scope_id": CONV}, "autonomy": "safe", "budgets": {"tool_calls_per_run": 2}})
call(router, "POST", "/hux/v1/runs/run_9f/budget", {"conversation_id": CONV, "tool_calls": 2}, WORKER)
status, body = gate(router, capability="shell")
assert body == {"proceed": False, "reason": "budget_exhausted", "exhausted": ["tool_calls_per_run"]}
assert events[-1]["kind"] == "budget.exhausted" and events[-1]["run_id"] == "run_9f" and events[-1]["conversation_id"] == CONV
assert gate(router, run_id="run_fresh", capability="shell")[1]["proceed"] is False, "unknown runs inherit nothing"
call(router, "POST", "/hux/v1/runs/run_fresh/budget", {"conversation_id": CONV}, WORKER)
assert gate(router, run_id="run_fresh", capability="shell")[1]["reason"] == "budget_exhausted"
status, error = call(router, "POST", "/hux/v1/approvals", request_body("shell", run_id="run_fresh"), WORKER)
assert (status, error["code"]) == (429, "budget_exhausted")
call(router, "POST", "/hux/v1/runs/run_other/budget", {"conversation_id": "conv_other0001"}, WORKER)
assert gate(router, run_id="run_other", capability="shell")[1]["proceed"] is False
assert budgets.run_conversation(router_store(router), "run_nobody") is None
def test_conversation_budget_aggregates_across_runs_and_policy_revision_resets(router):
"""Run rotation cannot reset spend; an explicit human policy revision starts a new budget epoch."""
policy_body = {
"scope": {"level": "conversation", "scope_id": CONV},
"autonomy": "safe",
"budgets": {"tool_calls_per_run": 3},
}
assert call(router, "PUT", "/hux/v1/policy", policy_body)[0] == 200
first = call(router, "POST", "/hux/v1/runs/run_a/budget", {"conversation_id": CONV, "tool_calls": 2}, WORKER)[1]
second = call(router, "POST", "/hux/v1/runs/run_b/budget", {"conversation_id": CONV, "tool_calls": 1}, WORKER)[1]
assert first["spent"]["tool_calls"] == 2
assert second["spent"]["tool_calls"] == 3 and second["exhausted"] == ["tool_calls_per_run"]
assert gate(router, run_id="run_b", capability="shell")[1]["reason"] == "budget_exhausted"
revised = call(router, "PUT", "/hux/v1/policy", policy_body)[1]
assert revised["revision"] == 2
reset = call(router, "GET", "/hux/v1/runs/run_b/budget", headers=WORKER)[1]
assert reset["spent"]["tool_calls"] == 0 and reset["exhausted"] == []
restarted = call(router, "POST", "/hux/v1/runs/run_b/budget", {"conversation_id": CONV, "tool_calls": 1}, WORKER)[1]
assert restarted["spent"]["tool_calls"] == 1, "the old epoch's per-run spend does not leak into the new epoch"
def router_store(router):
return store.TenantStore(router.data_root, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))