HUX-06/HUX-10: a conversation whose stored friendly mode is private now has network, web_search, send_message, shell and delegate refused by both the approval resolver and the pre-side-effect gate, regardless of autonomy policy — matching the mode catalog's tool contract. Memory writes were already refused by the privacy state. Missing or unbound conversations keep the existing matrix behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
129 lines
5.4 KiB
Python
129 lines
5.4 KiB
Python
"""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"
|