Scoped policies resolved through the single capability matrix, grants with server-set expiry, approval queue with once/session/always/deny, pre-side-effect gate that releases a once approval exactly once against the argument hash, run budgets with exhaustion events, honest cancellation receipts; projects, conversations, branch lineage and search over the indexed fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNPhwu2bsaRNg3DETSAZoM
204 lines
10 KiB
Python
204 lines
10 KiB
Python
"""HUX-05 run controls: budgets, the pre-side-effect gate and cancellation receipts.
|
|
|
|
The agent hook reports spend per run and asks the gate before every side
|
|
effect. The gate releases only against an approved, unexpired approval for the
|
|
same run, capability and argument hash; a ``once`` approval is consumed by its
|
|
first release (SO-36, SO-37). A stop is not done until a receipt says what
|
|
actually happened to the run and its side effects (SO-41).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from hux import policy, rules
|
|
from hux.errors import Invalid
|
|
from hux.http import Request, Response, Router
|
|
from hux.identity import Identity
|
|
from hux.store import TenantStore, check_id
|
|
|
|
BUDGETS = "budgets"
|
|
RECEIPTS = "receipts"
|
|
SPEND_KEYS = ("tokens", "tool_calls", "wall_clock_seconds", "delegations", "spend_units", "subagents")
|
|
LIMIT_OF = {
|
|
"tokens": "tokens_per_run", "tool_calls": "tool_calls_per_run", "wall_clock_seconds": "wall_clock_seconds",
|
|
"delegations": "delegations_per_run", "spend_units": "spend_units", "subagents": "subagents_per_run",
|
|
}
|
|
|
|
|
|
def run_id_from(request: Request) -> str:
|
|
"""The run id in the path; the router already bounded its alphabet."""
|
|
run_id = request.params["id"]
|
|
if len(run_id) > 120:
|
|
raise Invalid("run id too long")
|
|
return run_id
|
|
|
|
|
|
# -- budgets -------------------------------------------------------------------
|
|
|
|
def exhausted(spent: dict[str, int], limits: dict[str, Any]) -> list[str]:
|
|
"""Limit names whose spend has reached them; a zero limit is exhausted immediately."""
|
|
return [LIMIT_OF[k] for k in SPEND_KEYS if LIMIT_OF[k] in limits and spent.get(k, 0) >= limits[LIMIT_OF[k]]]
|
|
|
|
|
|
def budget_state(store: TenantStore, identity: Identity, run_id: str, conversation_id: str | None = None) -> dict[str, Any]:
|
|
"""Current ``hux.budget_state.v1`` for a run measured against the effective policy."""
|
|
doc_id = f"bud_{policy.run_key(run_id)}"
|
|
stored = store.get(BUDGETS, doc_id) if store.exists(BUDGETS, doc_id) else {"spent": {}, "_conversation_id": None}
|
|
conversation_id = conversation_id or stored.get("_conversation_id")
|
|
level, scope_id = ("conversation", conversation_id) if conversation_id else ("global", None)
|
|
limits = {k: v for k, v in policy.effective_policy(store, identity, level, scope_id)["budgets"].items() if k != "scope"}
|
|
spent = {k: int(stored["spent"].get(k, 0)) for k in SPEND_KEYS}
|
|
return policy.checked({
|
|
"schema": "hux.budget_state.v1", "run_id": run_id[:120], "spent": spent, "limits": limits,
|
|
"exhausted": exhausted(spent, limits), "_conversation_id": conversation_id, "_id": doc_id,
|
|
})
|
|
|
|
|
|
def get_budget(request: Request) -> Response:
|
|
"""``GET /hux/v1/runs/{id}/budget``: spend so far and what is exhausted."""
|
|
state = budget_state(request.store, request.identity, run_id_from(request))
|
|
request.audit("budgets.read", state["_id"])
|
|
return Response(200, policy.public(state))
|
|
|
|
|
|
def post_budget(request: Request) -> Response:
|
|
"""``POST /hux/v1/runs/{id}/budget``: add spend increments; emits budget.exhausted on the crossing."""
|
|
body = policy.body_dict(request)
|
|
run_id = run_id_from(request)
|
|
conversation_id = body.get("conversation_id")
|
|
if conversation_id is not None:
|
|
conversation_id = check_id(conversation_id)
|
|
increments = {}
|
|
for key in SPEND_KEYS:
|
|
value = body.get(key, 0)
|
|
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
raise Invalid(f"{key} must be a non-negative integer")
|
|
increments[key] = value
|
|
with request.store.lock(BUDGETS):
|
|
before = budget_state(request.store, request.identity, run_id, conversation_id)
|
|
known = before["exhausted"] if request.store.exists(BUDGETS, before["_id"]) else []
|
|
spent = {k: before["spent"][k] + increments[k] for k in SPEND_KEYS}
|
|
doc = {"id": before["_id"], "run_id": run_id, "spent": spent, "_conversation_id": before["_conversation_id"]}
|
|
request.store.put(BUDGETS, doc)
|
|
after = budget_state(request.store, request.identity, run_id, conversation_id)
|
|
request.audit("budgets.write", after["_id"])
|
|
newly = [k for k in after["exhausted"] if k not in known]
|
|
if newly:
|
|
policy.emit(request.store, request.identity, after["_conversation_id"], "budget.exhausted",
|
|
f"Budget exhausted: {', '.join(newly)}", run_id=run_id)
|
|
return Response(200, policy.public(after))
|
|
|
|
|
|
# -- gate ----------------------------------------------------------------------
|
|
|
|
def hashes_of(approval: dict[str, Any]) -> set[str]:
|
|
"""Argument hashes the approval was requested for (tool_call evidence with a hash)."""
|
|
return {e["hash"] for e in approval["request"].get("evidence", []) if e.get("kind") == "tool_call" and e.get("hash")}
|
|
|
|
|
|
def matching_approval(store: TenantStore, run_id: str, capability: str, argument_hash: str, external: bool, conversation_id: str | None) -> tuple[dict[str, Any] | None, str, str | None]:
|
|
"""The approval that releases this side effect, or why none does, plus the run's conversation when an approval names it."""
|
|
reason = "no approval for this run and capability"
|
|
for record in store.scan(policy.APPROVALS):
|
|
record = policy.refresh(store, record)
|
|
same_run = record["run_id"] == run_id
|
|
if same_run:
|
|
conversation_id = conversation_id or record["conversation_id"]
|
|
same_conv = bool(conversation_id) and record["conversation_id"] == conversation_id
|
|
choice = record.get("decision", {}).get("choice")
|
|
if record["capability"] != capability or not (same_run or (choice == "session" and same_conv)):
|
|
continue
|
|
if record["status"] != "approved":
|
|
reason = f"approval {record['id']} is {record['status']}"
|
|
continue
|
|
if policy.parse(record["expires_at"]) <= policy.now():
|
|
reason = f"approval {record['id']} has expired"
|
|
continue
|
|
if external and not record["request"]["external"]:
|
|
reason = f"approval {record['id']} was not requested as external"
|
|
continue
|
|
if choice == "once" and argument_hash not in hashes_of(record):
|
|
reason = f"approval {record['id']} was for different arguments"
|
|
continue
|
|
if choice == "once" and record.get("_consumed_at"):
|
|
reason = f"approval {record['id']} was already consumed"
|
|
continue
|
|
return record, "released", conversation_id
|
|
return None, reason, conversation_id
|
|
|
|
|
|
def gate(request: Request) -> Response:
|
|
"""``POST /hux/v1/runs/{id}/gate``: may this side effect proceed right now?"""
|
|
body = policy.body_dict(request)
|
|
run_id = run_id_from(request)
|
|
capability = body.get("capability")
|
|
argument_hash = body.get("argument_hash")
|
|
if capability not in rules.CAPABILITIES:
|
|
raise Invalid("unknown capability")
|
|
if not isinstance(argument_hash, str) or not argument_hash.startswith("sha256:"):
|
|
raise Invalid("argument_hash must be sha256:<hex>")
|
|
external = bool(body.get("external", False))
|
|
conversation_id = body.get("conversation_id")
|
|
if conversation_id is not None:
|
|
conversation_id = check_id(conversation_id)
|
|
with request.store.lock(policy.APPROVALS):
|
|
record, reason, conversation_id = matching_approval(request.store, run_id, capability, argument_hash, external, conversation_id)
|
|
if record is not None and record["decision"]["choice"] == "once":
|
|
request.store.put(policy.APPROVALS, {**record, "_consumed_at": policy.iso(policy.now())})
|
|
if record is None:
|
|
request.audit("gate.check", f"{run_id}:{capability}", outcome="deny", reason="policy_violation")
|
|
policy.emit(request.store, request.identity, conversation_id, "side_effect.blocked", f"{capability} blocked: {reason}"[:280], run_id=run_id)
|
|
return Response(200, {"proceed": False, "reason": reason})
|
|
request.audit("gate.check", record["id"])
|
|
policy.emit(request.store, request.identity, conversation_id, "side_effect.released", f"{capability} released by approval {record['id']}",
|
|
run_id=run_id, evidence=[{"kind": "approval", "id": record["id"]}])
|
|
return Response(200, {"proceed": True, "approval_id": record["id"], "reason": reason})
|
|
|
|
|
|
# -- stop ----------------------------------------------------------------------
|
|
|
|
def stop(request: Request) -> Response:
|
|
"""``POST /hux/v1/runs/{id}/stop``: write the cancellation receipt; a repeat returns it."""
|
|
body = policy.body_dict(request)
|
|
run_id = run_id_from(request)
|
|
receipt_id = f"rcpt_{policy.run_key(run_id)}"
|
|
with request.store.lock(RECEIPTS):
|
|
if request.store.exists(RECEIPTS, receipt_id):
|
|
existing = request.store.get(RECEIPTS, receipt_id)
|
|
request.audit("runs.stop", receipt_id, reason="replayed")
|
|
return Response(200, policy.public(existing))
|
|
side_effects = body.get("side_effects", [])
|
|
if not isinstance(side_effects, list):
|
|
raise Invalid("side_effects must be a list")
|
|
if body.get("already_complete"):
|
|
outcome = "already_complete"
|
|
elif body.get("process_registry_empty") is True:
|
|
outcome = "cancelled"
|
|
else:
|
|
outcome = "failed_to_cancel"
|
|
stamp = policy.iso(policy.now())
|
|
record: dict[str, Any] = {
|
|
"schema": "hux.cancel_receipt.v1", "id": receipt_id, "run_id": run_id, "requested_by": policy.actor_for(request.identity),
|
|
"requested_at": stamp, "acknowledged_at": stamp, "outcome": outcome, "side_effects": side_effects,
|
|
}
|
|
if outcome != "failed_to_cancel":
|
|
record["completed_at"] = stamp
|
|
conversation_id = body.get("conversation_id")
|
|
if conversation_id is not None:
|
|
record["conversation_id"] = check_id(conversation_id)
|
|
stored = request.store.put(RECEIPTS, policy.checked(record))
|
|
request.audit("runs.stop", receipt_id, reason=outcome)
|
|
policy.emit(request.store, request.identity, record.get("conversation_id"), "run.cancelled", f"Run stopped: {outcome}",
|
|
run_id=run_id, evidence=[{"kind": "run", "id": receipt_id}])
|
|
return Response(201, policy.public(stored))
|
|
|
|
|
|
def register_routes(router: Router) -> None:
|
|
"""Attach the run-scoped HUX-05 routes; called by ``hux.policy.register``."""
|
|
router.add("GET", "/hux/v1/runs/{id}/budget", policy.CARD, "budgets.read", get_budget)
|
|
router.add("POST", "/hux/v1/runs/{id}/budget", policy.CARD, "budgets.write", post_budget)
|
|
router.add("POST", "/hux/v1/runs/{id}/gate", policy.CARD, "gate.check", gate)
|
|
router.add("POST", "/hux/v1/runs/{id}/stop", policy.CARD, "runs.stop", stop)
|
|
|