# hux_hook: wiring notes for the agent runtime patch `hux_hook` is a stdlib-only library the Hermes agent process imports. HUX (`hermes-hux-foundation`, loopback `127.0.0.1:8790` in the same pod) is the source of truth; the agent never decides an approval, never reads the tenant ledger directly and never persists raw tool arguments or output. ## Environment the agent process needs | Variable | Purpose | | --- | --- | | `HUX_BASE_URL` | default `http://127.0.0.1:8790`; the service is loopback-only | | `HUX_TENANT_SLOT` | `slot-N`, same value the service was started with (`HUX_TENANT_SLOT` on the service side) | | `HUX_SUBJECT` | `usr_` of the slot owner; the router derives it, the pod env carries it | | `HUX_WORKER_KEY` | shared key mounted read-only under `/runtime-access`; sent as `X-Hux-Relay-Key` | | `HUX_SURFACE` / `HUX_TRUST` | default `worker` / `worker`; the agent hook is never a human surface (SO-35) | | `HUX_TIMEOUT_SECONDS` | default 5 | Headers the client sends (exactly `hux/identity.py`): `X-Hermes-Tenant-Identity`, `X-Hux-Subject`, `X-Hux-Surface`, `X-Hux-Trust`, `X-Hux-Relay-Key`, plus `Idempotency-Key` on creates and `If-Match` on revisioned PUTs. ## Construction ```python from hux_hook import HuxClient client = HuxClient(os.environ.get("HUX_BASE_URL", "http://127.0.0.1:8790"), {"tenant_slot": os.environ["HUX_TENANT_SLOT"], "subject": os.environ["HUX_SUBJECT"], "surface": "worker", "trust": "worker"}, key=os.environ.get("HUX_WORKER_KEY"), timeout=float(os.environ.get("HUX_TIMEOUT_SECONDS", "5"))) ``` One client per process. `client.capabilities()` is cached; call `client.forget_capabilities()` on SIGHUP or when the WebUI reports a flag change. ## Where each call goes in the tool loop 1. Run start: `emit(client, conversation_id, "run.started", "...", run_id=run_id)`. 2. Immediately before executing any tool that is not read-only: `d = before_tool(client, run_id, conversation_id, tool_name, arguments, capability, external, risk)`. - `d.proceed is True`: execute now, with exactly the `arguments` object that was hashed. Re-serialising or "normalising" arguments after the gate breaks SO-37. - `d.reason == "approval_required"`: do not execute. Surface `d.approval_id` to the UI (chat posts `POST /hux/v1/approvals/{id}` with `once|session|always|deny` from a human surface), park the turn, and call `before_tool` again with the same arguments after the decision. The idempotent replay reaches the gate; a `once` approval releases exactly once. - Any other reason (`approval_denied`, `budget_exhausted`, `hux_unavailable`, `autonomy_off`, `service_error:*`, a gate reason): refuse the tool and tell the model why. Never fall back to the upstream gateway approval prompt while `hux.autonomy` is on. Capability mapping is the runtime's job: `read_files`, `write_files`, `shell`, `network`, `web_search`, `send_message`, `memory_write`, `artifact_write`, `spend_tokens`, `delegate`, `deploy`, `external_side_effect` (see `hux/rules.py`). `external=True` for anything that leaves the tenant (messages, network, deploy). 3. After every tool: `after_tool(client, run_id, conversation_id, tool_name, ok, bytes_out, turn, argument_hash=canonical_argument_hash(tool_name, arguments), duration_ms=..., exit_code=...)` then `record_spend(client, run_id, conversation_id, tool_calls=1, tokens=)`. Both are best-effort: they return `None` on failure and never raise. 4. Delegation: `record_spend(..., delegations=1)` / `subagents=1` when spawning; the child run uses its own `run_id` and the same `conversation_id`. 5. Memory: before proposing a memory write call `memory_gate(client, conversation_id)`; `False` means do not even propose. The write itself still goes through `POST /hux/v1/memory`, which enforces forget, disable and topic rules server-side. 6. Stop: when the user cancels, kill the tool processes, then `receipt = on_stop(client, run_id, conversation_id, process_registry_empty=, side_effects=[{"description": ..., "reverted": bool}, ...])`. `None` means no receipt exists and the stop is NOT done: retry, and never report "cancelled" to the user without a receipt (SO-41). `process_registry_empty` must come from the gateway's process registry, not from a timer. 7. Run end: `emit(..., "run.completed" | "run.failed", ...)`. ## What must never happen - Raw arguments, file contents, command output, prompts or secrets in any `summary`, `detail` or `evidence`. The library only ever sends tool name, capability, hash, byte counts, status. - Deciding an approval with the worker identity (the service answers 403; do not retry as `chat`). - Executing a tool after `proceed=False` for any reason, including HUX being unreachable. - Logging `HuxServiceError` bodies with request payloads: exceptions carry status/code/message only. ## Routes used `GET /hux/v1/capabilities`, `POST /hux/v1/approvals`, `POST /hux/v1/runs/{id}/gate`, `POST /hux/v1/runs/{id}/budget`, `POST /hux/v1/runs/{id}/stop`, `POST /hux/v1/conversations/{id}/events`, `GET /hux/v1/privacy/policy`, `GET /hux/v1/conversations/{id}`.