jenkins 3ebee2cc15 hermes(hux): HUX-05 autonomy engine and minimal HUX-03 organization API
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
2026-08-24 00:48:20 -03:00

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)