atlas-iac/testing/tests/test_hermes_hux_policy_matrix.py

223 lines
13 KiB
Python

"""HUX-05 policy documents: the capability matrix, grants and their expiry.
Security obligations exercised: SO-38 (``always`` grants carry a server-set
expiry of at most 30 days), SO-39 (``effective_decision`` is the sole
resolver and deploy/external always ask), SO-44 (If-Match on revisioned
writes; unconditional writes are audited).
"""
from __future__ import annotations
import json
import sys
from datetime import datetime, timedelta, timezone
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 audit, contracts, errors, identity, policy, rules, store # noqa: E402
from hux.server import build_router # noqa: E402
SCHEMAS = contracts.load_all()
HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
OTHER = {**HEADERS, "X-Hux-Subject": "usr_fedcba9876543210"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
T0 = datetime(2026, 8, 24, 12, 0, tzinfo=timezone.utc)
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
@pytest.fixture
def frozen(monkeypatch):
state = {"now": T0}
monkeypatch.setattr(policy, "clock", lambda: state["now"])
return state
def call(router, method, path, body=None, headers=HEADERS):
raw = b"" if body is None else json.dumps(body).encode()
response = router.dispatch(method, path, headers, raw)
return response.status, response.body, response.headers
def valid(record):
assert contracts.validate_record(record, SCHEMAS) == [], contracts.validate_record(record, SCHEMAS)
return record
def test_modules_stay_under_the_line_budget():
for name in ("policy.py", "budgets.py"):
assert len((FOUNDATION / "hux" / name).read_text().splitlines()) <= 500
def test_first_read_creates_the_safe_global_default(router, tmp_path):
status, body, headers = call(router, "GET", "/hux/v1/policy")
assert status == 200 and headers["ETag"] == "1"
valid(body)
assert body["scope"] == {"level": "global"} and body["autonomy"] == "safe" and body["grants"] == []
assert body["owner"] == HEADERS["X-Hux-Subject"] and body["provenance"]["actor"] == {"type": "user", "id": body["owner"]}
assert call(router, "GET", "/hux/v1/policy?scope=global")[1] == body
rows = audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})))
assert [r["action"] for r in rows] == ["policy.read", "policy.read"]
def test_matrix_is_the_single_resolver_and_external_always_asks(router):
matrix = rules.default_capability_matrix()
for level in ("ask_first", "safe", "autonomous"):
put = {"scope": {"level": "global"}, "autonomy": level, "grants": [{"capability": "deploy", "decision": "allow"}]}
status, body, _ = call(router, "PUT", "/hux/v1/policy", put)
assert status == 200, body
for capability in rules.CAPABILITIES:
expected = matrix[level][capability]
if capability == "deploy":
expected = "ask" # an explicit allow on an always-ask capability still asks (SO-39)
assert policy.resolve_request(body, capability, external=False) == expected
assert policy.resolve_request(body, capability, external=True) in {"ask", "deny"}
assert policy.resolve_request(body, "network", external=True) == "ask"
def test_grants_get_server_set_expiry_and_actor(router, frozen):
far = policy.iso(T0 + timedelta(days=90))
soon = policy.iso(T0 + timedelta(days=2))
put = {"scope": {"level": "global"}, "autonomy": "safe", "grants": [
{"capability": "network", "decision": "allow", "expires_at": far, "granted_by": {"type": "operator", "id": "evil"}},
{"capability": "shell", "decision": "allow", "expires_at": soon},
{"capability": "write_files", "decision": "deny"},
]}
status, body, _ = call(router, "PUT", "/hux/v1/policy", put)
assert status == 200
valid(body)
by_cap = {g["capability"]: g for g in body["grants"]}
assert by_cap["network"]["expires_at"] == policy.iso(T0 + policy.ALWAYS_TTL)
assert by_cap["shell"]["expires_at"] == soon
assert by_cap["write_files"]["expires_at"] == policy.iso(T0 + policy.ALWAYS_TTL)
assert all(g["granted_by"] == {"type": "user", "id": HEADERS["X-Hux-Subject"]} for g in body["grants"])
assert policy.resolve_request(body, "network", False) == "allow"
assert policy.resolve_request(body, "shell", False) == "allow"
assert policy.resolve_request(body, "write_files", False) == "deny"
frozen["now"] = T0 + timedelta(days=3)
assert policy.resolve_request(body, "shell", False) == "ask", "expired grant falls back to the matrix"
assert policy.resolve_request(body, "network", False) == "allow"
frozen["now"] = T0 + timedelta(days=31)
assert policy.resolve_request(body, "network", False) == "deny", "safe denies network once the grant lapses"
def test_put_honours_if_match_and_audits_unconditional_writes(router, tmp_path):
put = {"scope": {"level": "global"}, "autonomy": "autonomous"}
status, body, headers = call(router, "PUT", "/hux/v1/policy", put)
assert (status, body["revision"], headers["ETag"]) == (200, 1, "1")
status, body, _ = call(router, "PUT", "/hux/v1/policy", put, {**HEADERS, "If-Match": "1"})
assert (status, body["revision"]) == (200, 2)
status, body, _ = call(router, "PUT", "/hux/v1/policy", put, {**HEADERS, "If-Match": "1"})
assert (status, body["code"]) == (409, "conflict")
status, body, _ = call(router, "PUT", "/hux/v1/policy", put)
assert (status, body["revision"]) == (200, 3)
rows = [(r["action"], r["outcome"], r.get("reason", "")) for r in audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})))]
assert rows == [("policy.write", "allow", ""), ("policy.write", "allow", ""), ("policy.write", "conflict", rows[2][2]), ("policy.write", "allow", "unconditional_write")]
assert "does not match" in rows[2][2]
@pytest.mark.parametrize("body,fragment", [
([], "JSON object"),
({"scope": "global"}, "scope must be an object"),
({"scope": {"level": "planet"}, "autonomy": "safe"}, "scope must be"),
({"scope": {"level": "project"}, "autonomy": "safe"}, "scope_id required"),
({"scope": {"level": "project", "scope_id": "prj_" + "a" * 70}, "autonomy": "safe"}, "scope_id required"),
({"scope": {"level": "project", "scope_id": "../etc"}, "autonomy": "safe"}, "malformed id"),
({"scope": {"level": "global"}, "autonomy": "yolo"}, "autonomy must be"),
({"scope": {"level": "global"}, "autonomy": "safe", "grants": "all"}, "grants must be a list"),
({"scope": {"level": "global"}, "autonomy": "safe", "grants": [{"capability": "teleport", "decision": "allow"}]}, "known capability"),
({"scope": {"level": "global"}, "autonomy": "safe", "grants": [{"capability": "shell", "decision": "maybe"}]}, "grant decision"),
({"scope": {"level": "global"}, "autonomy": "safe", "budgets": {"tokens_per_run": -1}}, "fails contract"),
({"scope": {"level": "global"}, "autonomy": "safe", "budgets": {"unknown": 1}}, "fails contract"),
])
def test_put_rejects_bad_bodies(router, body, fragment):
status, error, _ = call(router, "PUT", "/hux/v1/policy", body)
assert status == 400 and fragment in error["message"], error
valid(error)
def test_get_rejects_bad_scope_queries(router):
assert call(router, "GET", "/hux/v1/policy?scope=nope")[0] == 400
assert call(router, "GET", "/hux/v1/policy?scope=conversation")[0] == 400
def test_effective_policy_walks_conversation_project_global(router):
_, project, _ = call(router, "POST", "/hux/v1/projects", {"name": "Kitchen"})
_, conversation, _ = call(router, "POST", "/hux/v1/conversations", {"title": "Cabinets", "project_id": project["id"]})
_, loose, _ = call(router, "POST", "/hux/v1/conversations", {"title": "Unfiled"})
conv_query = f"/hux/v1/policy?scope=conversation&scope_id={conversation['id']}"
assert call(router, "GET", conv_query)[1]["scope"] == {"level": "global"}
status, body, _ = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "project", "scope_id": project["id"]}, "autonomy": "autonomous"})
assert status == 200 and valid(body)["id"] == f"pol_project.{project['id']}"
assert call(router, "GET", conv_query)[1]["scope"] == {"level": "project", "scope_id": project["id"]}
assert call(router, "GET", f"/hux/v1/policy?scope=conversation&scope_id={loose['id']}")[1]["scope"] == {"level": "global"}
assert call(router, "GET", "/hux/v1/policy?scope=conversation&scope_id=conv_unknown0001")[1]["scope"] == {"level": "global"}
status, body, _ = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "conversation", "scope_id": conversation["id"]}, "autonomy": "ask_first"})
assert status == 200
got = call(router, "GET", conv_query)[1]
assert got["autonomy"] == "ask_first" and got["scope"]["level"] == "conversation"
assert call(router, "GET", f"/hux/v1/policy?scope=project&scope_id={project['id']}")[1]["autonomy"] == "autonomous"
def test_second_subject_sees_its_own_default_not_the_first_tenants_policy(router):
call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "autonomous"})
status, body, _ = call(router, "GET", "/hux/v1/policy", headers=OTHER)
assert status == 200 and body["autonomy"] == "safe" and body["owner"] == OTHER["X-Hux-Subject"]
assert call(router, "GET", "/hux/v1/policy")[1]["autonomy"] == "autonomous"
def test_flag_off_hides_the_whole_card(tmp_path):
off = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation", "HUX_ROUTER_KEY": "rk"})
status, body, _ = call(off, "GET", "/hux/v1/policy")
assert (status, body["code"]) == (404, "flag_off")
def test_helpers_round_trip_time_and_actors():
assert policy.parse(policy.iso(T0)) == T0
assert policy.now().tzinfo is timezone.utc
worker = identity.Identity("slot-3", HEADERS["X-Hux-Subject"], "worker", "worker")
assert policy.actor_for(worker) == {"type": "system", "id": "worker"}
assert policy.run_key("run_9f") == policy.run_key("run_9f") and len(policy.run_key("x")) == 32
assert policy.public({"a": 1, "_b": 2, "revision": 3}) == {"a": 1}
assert policy.public({"a": 1, "_b": 2, "revision": 3}, revisioned=True) == {"a": 1, "revision": 3}
# --- F1: only humans write policy ---------------------------------------------------
def test_only_a_human_actor_may_write_policy_or_hold_allow_grants(tmp_path):
"""F1 (critical): worker and api surfaces cannot rewrite the policy, escalate autonomy or plant allow grants."""
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk", "HUX_RELAY_KEY": "rk"})
worker = {**HEADERS, "X-Hux-Surface": "worker", "X-Hux-Trust": "worker", "X-Hux-Relay-Key": "wk"}
api = {**HEADERS, "X-Hux-Surface": "api"}
relay = {**HEADERS, "X-Hux-Surface": "telegram", "X-Hux-Trust": "relay", "X-Hux-Relay-Key": "rk"}
escalate = {"scope": {"level": "global"}, "autonomy": "autonomous", "grants": [{"capability": "network", "decision": "allow"}]}
for headers in (worker, api):
status, body, _ = call(router, "PUT", "/hux/v1/policy", escalate, headers)
assert (status, body["code"]) == (403, "forbidden"), headers
status, body, _ = call(router, "PUT", "/hux/v1/policy", {"scope": {"level": "global"}, "autonomy": "safe"}, headers)
assert status == 403, "even a harmless-looking write is a policy write"
status, body, _ = call(router, "GET", "/hux/v1/policy", headers=api)
assert status == 200 and body["autonomy"] == "safe" and body["grants"] == [], "nothing leaked through"
status, body, _ = call(router, "PUT", "/hux/v1/policy", escalate, relay)
assert status == 200 and body["autonomy"] == "autonomous" and body["grants"][0]["granted_by"] == {"type": "user", "id": HEADERS["X-Hux-Subject"]}
rows = [(r["action"], r["outcome"]) for r in audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))) if r["action"] == "policy.write"]
assert rows == [("policy.write", "deny")] * 4 + [("policy.write", "allow")]
# The helpers assert the invariant even when a caller reaches them without the route.
system = identity.Identity("slot-3", HEADERS["X-Hux-Subject"], "worker", "worker")
with pytest.raises(errors.Forbidden):
policy.normalise_grants([{"capability": "shell", "decision": "allow"}], system)
assert policy.normalise_grants([{"capability": "shell", "decision": "deny"}], system)[0]["granted_by"] == {"type": "system", "id": "worker"}
with pytest.raises(errors.Forbidden):
policy.add_grant(store.TenantStore(tmp_path, system), system, "global", None, "shell", timedelta(hours=1))
assert policy.is_human(identity.Identity("slot-3", HEADERS["X-Hux-Subject"], "chat", "router"))
assert not policy.is_human(identity.Identity("slot-3", HEADERS["X-Hux-Subject"], "api", "router"))