"""HuxClient transport contract and the pure helpers of the agent hook. Security obligations exercised: SO-04 (the client emits exactly the identity header vocabulary ``hux.identity`` accepts, with the worker key), SO-07 (errors carry status, code and message only; request bodies are never embedded), SO-27 and SO-28 (memory writes need the privacy card and are refused in a private conversation), SO-37 (the argument hash is stable across key order and whitespace), SO-50 (capabilities are read from the resolved flag chain). """ from __future__ import annotations import ast import http.client import io import sys import threading import urllib.error import urllib.request from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] FOUNDATION = ROOT / "dockerfiles" / "hermes-hux-foundation" HOOK_ROOT = ROOT / "dockerfiles" / "hermes-worker-hux" for entry in (FOUNDATION, HOOK_ROOT): if str(entry) not in sys.path: sys.path.insert(0, str(entry)) from hux import contracts, identity # noqa: E402 from hux.http import serve # noqa: E402 from hux.server import build_router # noqa: E402 from hux_hook import HuxClient, HuxServiceError, HuxUnavailable, canonical_argument_hash, emit, memory_gate # noqa: E402 from hux_hook import client as client_mod # noqa: E402 from hux_hook import hooks # noqa: E402 SCHEMAS = contracts.load_all() ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"]) SUBJECT = "usr_0123456789abcdef" WORKER = {"tenant_slot": "slot-3", "subject": SUBJECT, "surface": "worker", "trust": "worker"} HUMAN = {"tenant_slot": "slot-3", "subject": SUBJECT, "surface": "chat", "trust": "router"} OTHER = {**HUMAN, "subject": "usr_fedcba9876543210"} CANARY = "CANARY-9c1d-SECRET" def start(tmp_path: Path, flags: str = ALL_ON): router = build_router(tmp_path, {"HUX_FLAGS": flags, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk"}) server = serve(router, "127.0.0.1", 0) threading.Thread(target=server.serve_forever, daemon=True).start() return f"http://127.0.0.1:{server.server_address[1]}", server @pytest.fixture def live(tmp_path): base, server = start(tmp_path) yield base, tmp_path server.shutdown() # --- headers and identity --------------------------------------------------------- def test_headers_match_the_service_vocabulary(): """SO-04: the exact header names identity.resolve reads, key only when present, extras only when non-empty.""" worker = HuxClient("http://127.0.0.1:1/", WORKER, key="wk") sent = worker.headers({"Idempotency-Key": "run:approval:1", "If-Match": ""}) assert identity.resolve(sent, {"HUX_WORKER_KEY": "wk"}) == identity.Identity("slot-3", SUBJECT, "worker", "worker") assert sent["Idempotency-Key"] == "run:approval:1" and "If-Match" not in sent assert worker.base_url == "http://127.0.0.1:1" plain = HuxClient(identity={"tenant_slot": "slot-3", "subject": SUBJECT}).headers() assert client_mod.HEADER_KEY not in plain and plain[client_mod.HEADER_TRUST] == "worker" assert HuxClient().identity == {"tenant_slot": "", "subject": "", "surface": "worker", "trust": "worker"} def test_client_loads_only_a_bounded_0400_key_file(tmp_path): """The hook can consume its projected worker key without an inline environment secret.""" key_file = tmp_path / "worker-key" key_file.write_text("wk\n") key_file.chmod(0o400) client = HuxClient("http://127.0.0.1:1", WORKER, key_file=key_file) assert client.headers()[client_mod.HEADER_KEY] == "wk" with pytest.raises(ValueError): HuxClient("http://127.0.0.1:1", WORKER, key="wk", key_file=key_file) key_file.chmod(0o444) with pytest.raises(ValueError): HuxClient("http://127.0.0.1:1", WORKER, key_file=key_file) def test_client_loads_router_bound_subject_from_file(tmp_path, monkeypatch): """The worker gets its subject only from the shared router binding and rejects conflicts.""" subject_file = tmp_path / "subject" subject_file.write_text(SUBJECT + "\n") subject_file.chmod(0o440) unbound_worker = {**WORKER, "subject": ""} assert HuxClient(identity=unbound_worker, subject_file=subject_file).identity["subject"] == SUBJECT assert HuxClient(identity=WORKER, subject_file=subject_file).identity["subject"] == SUBJECT with pytest.raises(ValueError, match="conflicts"): HuxClient(identity={**WORKER, "subject": OTHER["subject"]}, subject_file=subject_file) monkeypatch.setenv("HUX_SUBJECT_FILE", str(subject_file)) assert HuxClient(identity=unbound_worker).identity["subject"] == SUBJECT def test_client_subject_file_failures_are_closed(tmp_path): """Unavailable, linked, weak, malformed, oversized, and non-UTF-8 subject files are rejected.""" with pytest.raises(ValueError, match="unavailable"): HuxClient(identity=WORKER, subject_file=tmp_path / "missing") for name, payload, mode, message in ( ("empty", b"", 0o440, "empty or oversized"), ("oversized", b"x" * (client_mod.MAX_SUBJECT_BYTES + 1), 0o440, "empty or oversized"), ("unicode", b"\xff", 0o440, "not UTF-8"), ("malformed", b"brad@example.test", 0o440, "malformed"), ("weak", SUBJECT.encode(), 0o444, "0400 or 0440"), ): subject_file = tmp_path / name subject_file.write_bytes(payload) subject_file.chmod(mode) with pytest.raises(ValueError, match=message): HuxClient(identity=WORKER, subject_file=subject_file) valid = tmp_path / "valid" valid.write_text(SUBJECT) valid.chmod(0o400) linked = tmp_path / "linked" linked.symlink_to(valid) with pytest.raises(ValueError, match="unavailable"): HuxClient(identity=WORKER, subject_file=linked) def test_error_mapping_and_no_body_leak(live): """SO-07: a hux.error.v1 answer becomes HuxServiceError(status, code, message) and the body never appears in it.""" base, _ = live human = HuxClient(base, HUMAN, key="rk") worker = HuxClient(base, WORKER, key="wk") with pytest.raises(HuxServiceError) as bad: worker.post("/hux/v1/approvals", {"conversation_id": "conv_0001abcd", "capability": "nope", "secret": CANARY}) assert (bad.value.status, bad.value.code) == (400, "invalid") and CANARY not in str(bad.value) with pytest.raises(HuxServiceError) as unauth: HuxClient(base, WORKER, key="wrong").get("/hux/v1/capabilities") assert unauth.value.code == "unauthorized" with pytest.raises(HuxServiceError) as missing: human.get("/hux/v1/no/such/route") assert (missing.value.status, missing.value.code) == (404, "not_found") assert HuxUnavailable().code == "unavailable" and HuxUnavailable().status == 0 assert HuxServiceError(500, "x", "m" * 400).message == "m" * 280 def test_transport_edge_cases(monkeypatch): """Non-JSON failures, a 4xx delivered without an exception and socket errors map to typed errors.""" client = HuxClient("http://127.0.0.1:1", WORKER, key="wk") def raise_http(*args, **kwargs): raise urllib.error.HTTPError("u", 502, "bad gateway", {}, io.BytesIO(b"")) monkeypatch.setattr(client._opener, "open", raise_http) with pytest.raises(HuxServiceError) as html: client.get("/hux/v1/capabilities") assert (html.value.status, html.value.code) == (502, "invalid") class Raw: status = 418 headers = {"X-Test": "1"} def read(self, *_args): return b"{bad json" def __enter__(self): return self def __exit__(self, *exc): return False monkeypatch.setattr(client._opener, "open", lambda *a, **k: Raw()) with pytest.raises(HuxServiceError) as teapot: client.put("/hux/v1/policy", {"x": 1}, if_match=3) assert teapot.value.status == 418 def raise_socket(*args, **kwargs): raise TimeoutError("slow") monkeypatch.setattr(client._opener, "open", raise_socket) with pytest.raises(HuxUnavailable): client.get("/hux/v1/capabilities", query={"a": "b c"}) def raise_half_closed(*args, **kwargs): raise http.client.BadStatusLine("gone") monkeypatch.setattr(client._opener, "open", raise_half_closed) with pytest.raises(HuxUnavailable): client.get("/hux/v1/capabilities") @pytest.mark.parametrize("base", [ "https://127.0.0.1:8790", "http://localhost:8790", "http://10.0.0.1:8790", "http://user:secret@127.0.0.1:8790", "http://127.0.0.1:8790/path", "http://127.0.0.1:8790?next=x", "http://127.0.0.1", ]) def test_client_accepts_only_literal_loopback_origin(base): """The worker client refuses non-loopback, credentialed, ambiguous and TLS origins.""" with pytest.raises(ValueError): HuxClient(base, WORKER) assert HuxClient("http://[::1]:8790", WORKER).base_url == "http://[::1]:8790" with pytest.raises(ValueError): HuxClient("http://127.0.0.1:8790", WORKER, timeout=0) @pytest.mark.parametrize("path", ["https://evil.invalid/hux/v1/x", "//evil.invalid/x", "/hux/v1/../x", "/hux/v1/%2e%2e/x", "/hux/v1/x?y=1", "/healthz"]) def test_client_rejects_noncanonical_paths_and_redirects(path, monkeypatch): """Paths stay same-origin and the opener never creates a redirected credential-bearing request.""" monkeypatch.setattr(urllib.request, "getproxies", lambda: {"http": "http://proxy.invalid:8080"}) client = HuxClient("http://127.0.0.1:8790", WORKER, key=CANARY) with pytest.raises(HuxServiceError) as invalid: client.get(path) assert invalid.value.status == 400 assert client_mod._RejectRedirect().redirect_request(None, None, 302, "moved", {}, "https://evil.invalid") is None proxy_handlers = [handler for handler in client._opener.handlers if isinstance(handler, urllib.request.ProxyHandler)] assert proxy_handlers == [], "loopback credentials never enter an environment proxy" def test_client_bounds_sidecar_responses_and_non_string_paths(): """A compromised local sidecar cannot allocate an unbounded response or smuggle a non-string URL.""" class Oversized: def read(self, amount): return b"x" * amount with pytest.raises(HuxUnavailable, match="oversized response"): client_mod._bounded_read(Oversized()) with pytest.raises(HuxServiceError) as malformed: HuxClient("http://127.0.0.1:8790", WORKER).get(None) # type: ignore[arg-type] assert malformed.value.status == 400 def test_put_with_if_match_and_get_with_query(live): """Revisioned writes send If-Match; a stale revision is a conflict; queries reach the service.""" base, _ = live human = HuxClient(base, HUMAN, key="rk") first = human.put("/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "safe"}) assert first.header("ETag") == "1" and first.header("Missing") == "" second = human.put("/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "autonomous"}, if_match=1) assert second.body["revision"] == 2 with pytest.raises(HuxServiceError) as stale: human.put("/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "safe"}, if_match=1) assert stale.value.code == "conflict" assert human.get("/hux/v1/policy", {"scope": "global"}).body["autonomy"] == "autonomous" # --- capabilities --------------------------------------------------------------------- def test_capabilities_cached_per_process(live, monkeypatch): """SO-50: one capabilities read per process; refresh and forget re-read; failures are not cached.""" base, _ = live worker = HuxClient(base, WORKER, key="wk") calls = [] real_get = worker.get monkeypatch.setattr(worker, "get", lambda path, query=None: calls.append(path) or real_get(path, query)) assert worker.card_enabled("HUX-05") and worker.card_enabled("HUX-01") assert worker.capabilities()["contract_version"] == contracts.load_flags()["contract_version"] assert len(calls) == 1 worker.capabilities(refresh=True) worker.forget_capabilities() worker.capabilities() assert len(calls) == 3 monkeypatch.setattr(worker, "get", lambda path, query=None: (_ for _ in ()).throw(HuxServiceError(404, "flag_off", "off"))) worker.forget_capabilities() assert worker.capabilities() == {"reachable": True, "cards": {}, "contract_version": ""} dead = HuxClient("http://127.0.0.1:1", WORKER, key="wk", timeout=1) assert dead.capabilities()["reachable"] is False and not dead.card_enabled("HUX-11") # --- events --------------------------------------------------------------------------- def test_emit_is_best_effort_and_redaction_safe(live): """SO-11: detail outside the allowlist is dropped by the service; failures return None and never raise.""" base, root = live human = HuxClient(base, HUMAN, key="rk") conv = human.post("/hux/v1/conversations", {"title": "t"}).body["id"] worker = HuxClient(base, WORKER, key="wk") record = emit(worker, conv, "tool.call", "shell call", {"tool": "shell", "arguments": {"cmd": CANARY}, "argument_bytes": 7}, evidence=[{"kind": "run", "id": "run_1"}], run_id="run_1", turn=2, correlation_id="corr-1", idempotency_key="run_1:call:0001") assert contracts.validate_record(record, SCHEMAS) == [] and record["detail"] == {"tool": "shell", "argument_bytes": 7} assert record["provenance"]["actor"] == {"type": "system", "id": "hux-worker"} and record["correlation_id"] == "corr-1" assert emit(worker, conv, "tool.call", "again", idempotency_key="run_1:call:0001")["id"] == record["id"] assert emit(worker, conv, "not.a.kind", "x") is None assert emit(worker, "conv_unknown0001", "tool.call", "x") is None assert emit(worker, conv, "tool.call", "x", {"tool": object()}) is None assert emit(HuxClient(base, OTHER, key="rk"), conv, "tool.call", "cross tenant") is None private = human.post("/hux/v1/conversations", {"title": "p", "mode": "private"}).body["id"] assert emit(worker, private, "tool.call", "private mode writes nothing") is None assert CANARY not in "\n".join(p.read_text(errors="ignore") for p in root.rglob("*") if p.is_file()) # --- memory gate ------------------------------------------------------------------------ def test_memory_gate(live, tmp_path): """SO-27, SO-28: allowed only when the privacy card answers and the conversation is not private.""" base, _ = live human = HuxClient(base, HUMAN, key="rk") worker = HuxClient(base, WORKER, key="wk") normal = human.post("/hux/v1/conversations", {"title": "n", "mode": "thoughtful"}).body["id"] private = human.post("/hux/v1/conversations", {"title": "p", "mode": "private"}).body["id"] assert memory_gate(worker, normal) is True assert memory_gate(worker, private) is False assert memory_gate(worker, "conv_notknown01") is False assert memory_gate(worker, "bad id") is False human.post(f"/hux/v1/conversations/{normal}/forget", {}) assert memory_gate(worker, normal) is False assert memory_gate(HuxClient("http://127.0.0.1:1", WORKER, key="wk", timeout=1), normal) is False off_base, off_server = start(tmp_path / "off", "hux.foundation,hux.projects") try: assert memory_gate(HuxClient(off_base, WORKER, key="wk"), normal) is False finally: off_server.shutdown() # --- pure helpers ------------------------------------------------------------------------- def test_hash_is_canonical(): """SO-37: key order, whitespace and nesting order of dict keys do not change the hash; values do.""" a = canonical_argument_hash("write", {"path": "n.md", "opts": {"b": 1, "a": [1, 2]}}) b = canonical_argument_hash("write", {"opts": {"a": [1, 2], "b": 1}, "path": "n.md"}) assert a == b and a.startswith("sha256:") and len(a) == 71 assert canonical_argument_hash("write", {"path": "n.md ", "opts": {"b": 1, "a": [1, 2]}}) != a assert canonical_argument_hash("other", {"path": "n.md", "opts": {"b": 1, "a": [1, 2]}}) != a assert canonical_argument_hash("w", "raw string") == canonical_argument_hash("w", "raw string") assert hooks.canonical_json({"z": "é", "a": None}) == b'{"a":null,"z":"\\u00e9"}' def test_key_and_ref_helpers(): """Idempotency keys always satisfy the contract pattern; call refs never contain arguments.""" assert hooks.idempotency_key("r", "a", "") == "r:a:.pad" assert hooks.idempotency_key("r", "a", "x") == "r:a:x.pad" key = hooks.idempotency_key("run id/with spaces", "approval", "f" * 200) assert len(key) == 120 and " " not in key and "/" not in key assert hooks.call_ref("my tool/x", "sha256:" + "ab" * 32) == "my-tool-x:abababababababab" assert hooks._failure_reason(HuxServiceError(404, "flag_off", "")) == "flag_off" assert hooks._failure_reason(HuxServiceError(0, "unavailable", "")) == "hux_unavailable" assert hooks._failure_reason(HuxServiceError(409, "conflict", "")) == "service_error:conflict" def test_library_is_stdlib_only_small_and_documented(): """Every module ≤ 500 lines, every public function and module documented, no third-party imports.""" for path in sorted((HOOK_ROOT / "hux_hook").glob("*.py")): source = path.read_text() assert len(source.splitlines()) <= 500, path tree = ast.parse(source) assert ast.get_docstring(tree), path for node in ast.walk(tree): if isinstance(node, ast.FunctionDef | ast.ClassDef) and not node.name.startswith("_"): assert ast.get_docstring(node), f"{path.name}:{node.name}" if isinstance(node, ast.Import | ast.ImportFrom): root = (node.names[0].name if isinstance(node, ast.Import) else node.module or "").split(".")[0] assert root in {"http", "hmac", "json", "os", "stat", "threading", "urllib", "collections", "pathlib", "typing", "hashlib", "re", "dataclasses", "hux_hook", "__future__"}, root