"""Private mode denies web, messaging, shell and delegation upstream. HUX-06/HUX-10: selecting the private friendly mode must change what the agent may actually do — not merely relabel the conversation. The approval resolver and the pre-side-effect gate both refuse the denied capability set for a conversation whose stored mode is ``private``, whatever the autonomy policy says. """ from __future__ import annotations import json import sys 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)) from hux import contracts, policy # noqa: E402 from hux.server import build_router # noqa: E402 HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"} WORKER = {**HEADERS, "X-Hux-Surface": "worker", "X-Hux-Trust": "worker", "X-Hux-Relay-Key": "wk"} ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"]) HASH = "sha256:" + "ab" * 32 def call(router, method, path, body=None, headers=None): raw = b"" if body is None else json.dumps(body).encode() response = router.dispatch(method, path, {**HEADERS, **(headers or {})}, raw) return response.status, response.body @pytest.fixture def scoped(tmp_path): router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk"}) _, project = call(router, "POST", "/hux/v1/projects", {"name": "P"}) _, conversation = call(router, "POST", "/hux/v1/conversations", {"title": "C", "project_id": project["id"]}) status, _ = call( router, "POST", "/hux/v1/runs/run_9f/budget", {"conversation_id": conversation["id"]}, WORKER, ) assert status == 200 return router, project["id"], conversation["id"] def _select_mode(router, project_id, conversation_id, mode): status, body = call( router, "PUT", f"/hux/v1/projects/{project_id}/conversations/{conversation_id}/mode", {"project_id": project_id, "mode": mode}, {"If-Match": "0", "Idempotency-Key": f"mode-{mode}-0001"}, ) assert status == 200, body def _approval(router, conversation_id, capability, external=False): body = { "run_id": "run_9f", "conversation_id": conversation_id, "capability": capability, "request": {"summary": f"do {capability}", "risk": "low", "external": external, "evidence": [{"kind": "tool_call", "id": "call-7", "hash": HASH}]}, } return call(router, "POST", "/hux/v1/approvals", body, WORKER) def _gate(router, capability, external=False): return call( router, "POST", "/hux/v1/runs/run_9f/gate", {"capability": capability, "argument_hash": HASH, "external": external, "conversation_id": "ignored"}, WORKER, ) def test_private_mode_denies_the_catalog_denied_capabilities(scoped): router, project_id, conversation_id = scoped status, _ = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "autonomous"}) assert status == 200 _select_mode(router, project_id, conversation_id, "private") for capability in sorted(policy.PRIVATE_DENIED): status, body = _approval(router, conversation_id, capability) assert status == 201, body assert body["status"] == "denied", (capability, body) assert body["decision"]["by"] == {"type": "system", "id": "policy"} # Reading files and writing artifacts stay governed by the normal matrix. status, body = _approval(router, conversation_id, "read_files") assert status == 201 and body["status"] == "approved" def test_gate_refuses_private_mode_even_with_an_earlier_approval(scoped): router, project_id, conversation_id = scoped status, _ = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "autonomous"}) assert status == 200 status, approved = _approval(router, conversation_id, "network") assert status == 201 and approved["status"] == "approved" _select_mode(router, project_id, conversation_id, "private") status, verdict = _gate(router, "network") assert status == 200 assert verdict == {"proceed": False, "reason": "private_mode"} def test_non_private_modes_do_not_touch_the_matrix(scoped): router, project_id, conversation_id = scoped status, _ = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "autonomous"}) assert status == 200 _select_mode(router, project_id, conversation_id, "fast") status, body = _approval(router, conversation_id, "network") assert status == 201 and body["status"] == "approved" status, verdict = _gate(router, "network") assert status == 200 and verdict["proceed"] is True def test_unbound_or_missing_conversations_fail_open_to_the_matrix(tmp_path): router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk"}) status, _ = call( router, "POST", "/hux/v1/runs/run_9f/budget", {"conversation_id": "conv_0001abcd"}, WORKER, ) assert status == 200 status, _ = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "autonomous"}) assert status == 200 # No stored conversation record: the private check never blocks. status, body = _approval(router, "conv_0001abcd", "network") assert status == 201 and body["status"] == "approved"