security(hux): harden backend trust boundaries

This commit is contained in:
jenkins 2026-08-24 01:24:14 -03:00
parent 98c7c6184f
commit f59f24a427
29 changed files with 714 additions and 149 deletions

View File

@ -42,16 +42,28 @@ def exhausted(spent: dict[str, int], limits: dict[str, Any]) -> list[str]:
def budget_state(store: TenantStore, identity: Identity, run_id: str, conversation_id: str | None = None) -> dict[str, Any]:
"""Current ``hux.budget_state.v1`` for a run measured against the effective policy."""
"""Current state for a run, enforced against its conversation's policy epoch aggregate."""
doc_id = f"bud_{policy.run_key(run_id)}"
stored = store.get(BUDGETS, doc_id) if store.exists(BUDGETS, doc_id) else {"spent": {}, "_conversation_id": None}
conversation_id = conversation_id or stored.get("_conversation_id")
known_conversation = stored.get("_conversation_id") or run_conversation(store, run_id)
if known_conversation and conversation_id and known_conversation != conversation_id:
raise Invalid("run is already bound to another conversation")
conversation_id = known_conversation or conversation_id
level, scope_id = ("conversation", conversation_id) if conversation_id else ("global", None)
limits = {k: v for k, v in policy.effective_policy(store, identity, level, scope_id)["budgets"].items() if k != "scope"}
spent = {k: int(stored["spent"].get(k, 0)) for k in SPEND_KEYS}
effective = policy.effective_policy(store, identity, level, scope_id)
limits = {k: v for k, v in effective["budgets"].items() if k != "scope"}
epoch = str(effective.get("_budget_epoch") or f"{effective['id']}:{effective['revision']}")
run_spent = {k: int(stored["spent"].get(k, 0)) for k in SPEND_KEYS}
aggregate = {k: 0 for k in SPEND_KEYS}
for record in store.scan(BUDGETS):
if record.get("_conversation_id") != conversation_id or record.get("_budget_epoch") != epoch:
continue
for key in SPEND_KEYS:
aggregate[key] += int(record.get("spent", {}).get(key, 0))
return policy.checked({
"schema": "hux.budget_state.v1", "run_id": run_id[:120], "spent": spent, "limits": limits,
"exhausted": exhausted(spent, limits), "_conversation_id": conversation_id, "_id": doc_id,
"schema": "hux.budget_state.v1", "run_id": run_id[:120], "spent": aggregate, "limits": limits,
"exhausted": exhausted(aggregate, limits), "_conversation_id": conversation_id, "_id": doc_id,
"_run_spent": run_spent, "_budget_epoch": epoch,
})
@ -78,8 +90,11 @@ def post_budget(request: Request) -> Response:
with request.store.lock(BUDGETS):
before = budget_state(request.store, request.identity, run_id, conversation_id)
known = before["exhausted"] if request.store.exists(BUDGETS, before["_id"]) else []
spent = {k: before["spent"][k] + increments[k] for k in SPEND_KEYS}
doc = {"id": before["_id"], "run_id": run_id, "spent": spent, "_conversation_id": before["_conversation_id"]}
spent = {k: before["_run_spent"][k] + increments[k] for k in SPEND_KEYS}
doc = {
"id": before["_id"], "run_id": run_id, "spent": spent,
"_conversation_id": before["_conversation_id"], "_budget_epoch": before["_budget_epoch"],
}
request.store.put(BUDGETS, doc)
after = budget_state(request.store, request.identity, run_id, conversation_id)
request.audit("budgets.write", after["_id"])
@ -111,11 +126,11 @@ def run_conversation(store: TenantStore, run_id: str) -> str | None:
def matching_approval(store: TenantStore, run_id: str, capability: str, argument_hash: str, external: bool, conversation_id: str | None) -> tuple[dict[str, Any] | None, str, str | None]:
"""The approval that releases this side effect, or why none does, plus the conversation the gate settled on.
External effects release only against an approval for the same run and
the same argument hash, whatever the choice (F4, SO-39). Non-external
session/always approvals stay reusable inside their conversation, which
is the run's own conversation when the run is known and otherwise the
approval's. A ``once`` approval names exactly one hash (SO-36, SO-37).
Every release uses an approval record created for this exact run. A
session/always grant may let a later run create a new approved record,
but the old record itself never crosses run ids. External effects also
require the same argument hash; ``once`` names exactly one hash (SO-36,
SO-37, SO-39).
"""
reason = "no approval for this run and capability"
for record in store.scan(policy.APPROVALS):
@ -124,9 +139,7 @@ def matching_approval(store: TenantStore, run_id: str, capability: str, argument
continue
same_run = record["run_id"] == run_id
choice = record.get("decision", {}).get("choice")
reusable = choice in ("session", "always") and not external and not record["request"]["external"]
same_conv = record["conversation_id"] == (conversation_id or record["conversation_id"])
if not (same_run or (reusable and same_conv)):
if not same_run:
continue
if record["status"] != "approved":
reason = f"approval {record['id']} is {record['status']}"
@ -233,4 +246,3 @@ def register_routes(router: Router) -> None:
router.add("POST", "/hux/v1/runs/{id}/budget", policy.CARD, "budgets.write", post_budget)
router.add("POST", "/hux/v1/runs/{id}/gate", policy.CARD, "gate.check", gate)
router.add("POST", "/hux/v1/runs/{id}/stop", policy.CARD, "runs.stop", stop)

View File

@ -70,6 +70,16 @@ class TooLarge(HuxError):
status, code = 413, "too_large"
class RateLimited(HuxError):
"""The caller exceeded the bounded per-subject request rate."""
status, code = 429, "rate_limited"
def __init__(self, retry_after: int) -> None:
super().__init__("request rate limit exceeded")
self.retry_after = max(1, int(retry_after))
class ApprovalRequired(HuxError):
"""An action needs an approval record before it may proceed."""

View File

@ -41,7 +41,7 @@ CARD_ROUTES: dict[str, list[str]] = {
# artifact writes. The conversation read is there so the hook can honour
# private mode (SO-28) before proposing a memory.
WORKER_ROUTES: frozenset[tuple[str, str]] = frozenset({
("GET", "/hux/v1/capabilities"), ("GET", "/hux/v1/manifest"), ("GET", "/hux/v1/releases"),
("GET", "/hux/v1/capabilities"), ("GET", "/hux/v1/manifest"),
("POST", "/hux/v1/approvals"),
("POST", "/hux/v1/runs/{id}/gate"), ("POST", "/hux/v1/runs/{id}/budget"), ("POST", "/hux/v1/runs/{id}/stop"),
("GET", "/hux/v1/runs/{id}/budget"),
@ -66,9 +66,9 @@ class Flags:
self._registry = flag_registry()
def enabled(self, card: str) -> bool:
"""True when the card and its whole dependency chain are on."""
"""True only for a route-backed card whose configured flag chain is on."""
entry = self._registry.get(card)
return bool(entry) and flag_enabled(entry["flag"], self._environ)
return bool(entry) and bool(CARD_ROUTES.get(card)) and flag_enabled(entry["flag"], self._environ)
def require(self, card: str) -> None:
"""Raise FlagOff unless the card is enabled."""

View File

@ -9,7 +9,11 @@ Errors always leave as ``hux.error.v1``.
from __future__ import annotations
import json
import math
import os
import re
import threading
import time
from dataclasses import dataclass, field
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
@ -18,15 +22,55 @@ from collections.abc import Mapping
from urllib.parse import parse_qs, urlsplit
from hux import audit
from hux.errors import Forbidden, HuxError, Invalid, NotFound, TooLarge
from hux.errors import Forbidden, HuxError, Invalid, NotFound, RateLimited, TooLarge
from hux.flags import CONTRACT_VERSION, Flags, build_from_environ, worker_may_call
from hux.identity import Identity, resolve
from hux.store import TenantStore
MAX_BODY_BYTES = 1024 * 1024
DEFAULT_REQUEST_TIMEOUT_SECONDS = 10.0
DEFAULT_READS_PER_MINUTE = 300
DEFAULT_WRITES_PER_MINUTE = 30
MAX_RATE_BUCKETS = 4096
Handler = Callable[["Request"], "Response"]
def _bounded_number(environ: Mapping[str, str], name: str, default: float, low: float, high: float) -> float:
"""Read one numeric setting, using its safe default when malformed or outside bounds."""
try:
value = float(environ.get(name, default))
except (TypeError, ValueError):
return default
return value if math.isfinite(value) and low <= value <= high else default
class RateLimiter:
"""Small fixed-window limiter bounded by active subjects in the current minute."""
def __init__(self, reads: int, writes: int, clock: Callable[[], float] = time.monotonic) -> None:
self.limits = {"read": reads, "write": writes}
self.clock = clock
self._windows: dict[tuple[str, str], tuple[float, int]] = {}
self._lock = threading.Lock()
def check(self, subject: str, method: str) -> int | None:
"""Consume one request and return Retry-After seconds when its bucket is full."""
now = self.clock()
bucket = "read" if method in {"GET", "HEAD", "OPTIONS"} else "write"
key = (subject, bucket)
with self._lock:
# Each accepted identity can leave at most two current entries;
# expired identities disappear whenever any request arrives.
self._windows = {item: value for item, value in self._windows.items() if value[0] > now}
reset, used = self._windows.get(key, (now + 60.0, 0))
if key not in self._windows and len(self._windows) >= MAX_RATE_BUCKETS:
return 60
if used >= self.limits[bucket]:
return max(1, math.ceil(reset - now))
self._windows[key] = (reset, used + 1)
return None
@dataclass
class Request:
"""Everything a handler needs; no raw socket access."""
@ -102,10 +146,16 @@ class Router:
def __init__(self, data_root: Path, environ: Mapping[str, str] | None = None) -> None:
self.data_root = Path(data_root)
self.environ = environ
self.flags = Flags(environ)
self.build = build_from_environ(environ)
self.environ = dict(os.environ if environ is None else environ)
self.flags = Flags(self.environ)
self.build = build_from_environ(self.environ)
self.routes: list[Route] = []
reads = int(_bounded_number(self.environ, "HUX_READS_PER_MINUTE", DEFAULT_READS_PER_MINUTE, 1, 10_000))
writes = int(_bounded_number(self.environ, "HUX_WRITES_PER_MINUTE", DEFAULT_WRITES_PER_MINUTE, 1, 10_000))
self.request_timeout = _bounded_number(
self.environ, "HUX_REQUEST_TIMEOUT_SECONDS", DEFAULT_REQUEST_TIMEOUT_SECONDS, 0.1, 60.0
)
self.rate_limiter = RateLimiter(reads, writes)
def add(self, method: str, template: str, card: str, action: str, handler: Handler, max_body: int = MAX_BODY_BYTES) -> None:
"""Register a handler; ``action`` is the audit action name (family.verb), ``max_body`` its byte cap."""
@ -122,6 +172,11 @@ class Router:
return route, found.groupdict(), True
return None, {}, known
def body_limit(self, method: str, raw_path: str) -> int:
"""Maximum declared body for a matching route, before a socket read allocates it."""
route, _, _ = self.match(method, urlsplit(raw_path).path)
return route.max_body if route is not None else MAX_BODY_BYTES
def dispatch(self, method: str, raw_path: str, headers: Mapping[str, str], body: bytes) -> Response:
"""Run the full pipeline and never raise."""
parts = urlsplit(raw_path)
@ -141,6 +196,9 @@ class Router:
# worker cannot even learn which cards are on.
if identity.trust == "worker" and not worker_may_call(method, route.template):
raise Forbidden("route is not available to worker trust")
retry_after = self.rate_limiter.check(identity.subject, method)
if retry_after is not None:
raise RateLimited(retry_after)
self.flags.require(route.card)
payload = self._decode(body, route.max_body)
request = Request(method, parts.path, params, query, headers, payload, identity, store, self.flags, self.build)
@ -148,7 +206,8 @@ class Router:
except HuxError as error:
outcome = {"flag_off": "flag_off", "conflict": "conflict", "not_found": "not_found"}.get(error.code, "deny")
audit.record(store, identity, route.action, parts.path, outcome, error.message)
return Response(error.status, error.record())
headers = {"Retry-After": str(error.retry_after)} if isinstance(error, RateLimited) else {}
return Response(error.status, error.record(), headers)
except Exception: # noqa: BLE001 - the pipeline never raises; anything else is a 500 with no detail leaked
error = HuxError("internal error")
audit.record(store, identity, route.action, parts.path, "deny", error.message)
@ -182,18 +241,39 @@ def make_handler(router: Router) -> type[BaseHTTPRequestHandler]:
return
def _run(self) -> None:
if self.path == "/healthz":
self._send(Response(200, {"status": "ok", "contract_version": CONTRACT_VERSION}))
return
length = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(length) if length else b""
self._send(router.dispatch(self.command, self.path, dict(self.headers.items()), body))
try:
if self.headers.get("Transfer-Encoding"):
raise Invalid("Transfer-Encoding is not supported")
lengths = self.headers.get_all("Content-Length", [])
if len(lengths) > 1:
raise Invalid("duplicate Content-Length is not supported")
raw_length = lengths[0] if lengths else "0"
if not raw_length.isascii() or not raw_length.isdigit():
raise Invalid("Content-Length must be a non-negative integer")
if len(raw_length) > 10:
raise TooLarge("declared body length is too large")
length = int(raw_length)
limit = 0 if self.path == "/healthz" else router.body_limit(self.command, self.path)
if length > limit:
raise TooLarge(f"body exceeds {limit} bytes")
body = self.rfile.read(length) if length else b""
if len(body) != length:
raise Invalid("request body is incomplete or timed out")
if self.path == "/healthz":
self._send(Response(200, {"status": "ok", "contract_version": CONTRACT_VERSION}))
return
self._send(router.dispatch(self.command, self.path, dict(self.headers.items()), body))
except HuxError as error:
self._send(Response(error.status, error.record()))
except (OSError, TimeoutError):
error = Invalid("request body is incomplete or timed out")
self._send(Response(error.status, error.record()))
def _send(self, response: Response) -> None:
if response.stream is not None:
self.send_response(response.status)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("Cache-Control", "no-store")
for key, value in response.headers.items():
self.send_header(key, value)
self.end_headers()
@ -205,6 +285,7 @@ def make_handler(router: Router) -> type[BaseHTTPRequestHandler]:
self.send_response(response.status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
for key, value in response.headers.items():
self.send_header(key, value)
self.end_headers()
@ -215,8 +296,22 @@ def make_handler(router: Router) -> type[BaseHTTPRequestHandler]:
return HuxHandler
class BoundedHTTPServer(ThreadingHTTPServer):
"""Threading server that bounds header and body socket reads from accept onward."""
def __init__(self, address: tuple[str, int], handler: type[BaseHTTPRequestHandler], timeout: float) -> None:
self.request_timeout = timeout
super().__init__(address, handler)
def get_request(self) -> tuple[Any, Any]:
"""Accept one connection and apply its per-request socket deadline."""
request, address = super().get_request()
request.settimeout(self.request_timeout)
return request, address
def serve(router: Router, host: str = "127.0.0.1", port: int = 8790) -> ThreadingHTTPServer:
"""Create (but do not start) the server; callers call serve_forever()."""
server = ThreadingHTTPServer((host, port), make_handler(router))
server = BoundedHTTPServer((host, port), make_handler(router), router.request_timeout)
server.daemon_threads = True
return server

View File

@ -2,8 +2,8 @@
The chat router, the Telegram relay and the Worker are the only callers. Each
asserts identity in headers; the request body is never trusted for identity.
Relay and worker callers must also present their shared key, compared in
constant time against the value the pod was started with.
Router, relay, and worker callers must each present their distinct shared key,
compared in constant time against an inline or projected secret value.
"""
from __future__ import annotations
@ -13,6 +13,7 @@ import os
import re
from dataclasses import dataclass
from collections.abc import Mapping
from pathlib import Path
from hux.errors import Unauthorized
@ -26,7 +27,8 @@ SLOT_RE = re.compile(r"^slot-[0-9]{1,3}$")
SUBJECT_RE = re.compile(r"^usr_[0-9a-f]{16,64}$")
SURFACES = ("chat", "worker", "telegram", "voice", "api")
TRUSTS = ("router", "relay", "worker")
KEY_ENV = {"relay": "HUX_RELAY_KEY", "worker": "HUX_WORKER_KEY"}
KEY_ENV = {"router": "HUX_ROUTER_KEY", "relay": "HUX_RELAY_KEY", "worker": "HUX_WORKER_KEY"}
MAX_KEY_BYTES = 4096
@dataclass(frozen=True)
@ -50,11 +52,29 @@ def _header(headers: Mapping[str, str], name: str) -> str:
return ""
def _expected_key(environ: Mapping[str, str], trust: str) -> str:
"""Read a caller key, preferring a bounded Kubernetes secret file."""
name = KEY_ENV[trust]
key_file = environ.get(f"{name}_FILE", "")
if key_file:
try:
data = Path(key_file).read_bytes()
except OSError:
return ""
if len(data) > MAX_KEY_BYTES:
return ""
try:
return data.decode("utf-8", errors="strict").strip()
except UnicodeDecodeError:
return ""
return environ.get(name, "")
def resolve(headers: Mapping[str, str], environ: Mapping[str, str] | None = None) -> Identity:
"""Build an Identity from request headers or raise Unauthorized.
``trust`` defaults to ``router``; relay and worker trust require the
matching shared key to be configured and presented.
``trust`` defaults to ``router``; every trusted hop requires its distinct
shared key to be configured and presented.
"""
environ = os.environ if environ is None else environ
slot = _header(headers, HEADER_SLOT)
@ -70,11 +90,10 @@ def resolve(headers: Mapping[str, str], environ: Mapping[str, str] | None = None
raise Unauthorized("missing or malformed subject")
if surface not in SURFACES or trust not in TRUSTS:
raise Unauthorized("unknown surface or trust")
if trust in KEY_ENV:
expected = environ.get(KEY_ENV[trust], "")
presented = _header(headers, HEADER_KEY)
if not expected or not presented or not hmac.compare_digest(expected, presented):
raise Unauthorized(f"{trust} key missing or wrong")
expected = _expected_key(environ, trust)
presented = _header(headers, HEADER_KEY)
if not expected or not presented or not hmac.compare_digest(expected, presented):
raise Unauthorized(f"{trust} key missing or wrong")
if trust == "router" and surface == "worker":
raise Unauthorized("worker surface needs worker trust")
return Identity(slot, subject, surface, trust)

View File

@ -137,7 +137,7 @@ def default_policy(identity: Identity) -> dict[str, Any]:
return {
"schema": "hux.policy.v1", "id": "pol_global", "owner": identity.subject, "scope": {"level": "global"},
"autonomy": "safe", "grants": [], "budgets": dict(DEFAULT_BUDGETS),
"provenance": provenance(identity), "updated_at": iso(now()),
"provenance": provenance(identity), "updated_at": iso(now()), "_budget_epoch": new_id("bep"),
}
@ -210,6 +210,7 @@ def put_policy(request: Request) -> Response:
"schema": "hux.policy.v1", "id": record_id, "owner": request.identity.subject, "scope": scope,
"autonomy": body["autonomy"], "grants": normalise_grants(body.get("grants", []), request.identity),
"budgets": budgets, "provenance": provenance(request.identity), "updated_at": iso(now()),
"_budget_epoch": new_id("bep"),
}
expected = request.if_match()
with request.store.lock(FAMILY):
@ -232,9 +233,13 @@ def add_grant(store: TenantStore, identity: Identity, level: str, scope_id: str
record = {**global_policy(store, identity), "id": record_id, "scope": scope}
record.pop("revision")
expected = record.pop("revision", None)
budget_epoch = record.get("_budget_epoch") or f"{record['id']}:{expected or 1}"
grants = [g for g in record["grants"] if g["capability"] != capability]
grants.append({"capability": capability, "decision": "allow", "expires_at": iso(now() + min(ttl, ALWAYS_TTL)), "granted_by": actor_for(identity)})
record = {**record, "grants": grants[-64:], "provenance": provenance(identity), "updated_at": iso(now())}
record = {
**record, "grants": grants[-64:], "provenance": provenance(identity),
"updated_at": iso(now()), "_budget_epoch": budget_epoch,
}
store.put(FAMILY, checked(record), expected_revision=expected)
@ -283,6 +288,7 @@ def create_approval(request: Request) -> Response:
if capability not in rules.CAPABILITIES:
raise Invalid("unknown capability")
from hux import budgets # lazy: budgets imports this module
bound_conversation = budgets.run_conversation(request.store, str(body.get("run_id", "")))
state = budgets.budget_state(request.store, request.identity, str(body.get("run_id", "")), conversation_id)
if state["exhausted"]:
emit(request.store, request.identity, conversation_id, "budget.exhausted", f"Budget exhausted: {', '.join(state['exhausted'])}", run_id=state["run_id"])
@ -294,6 +300,9 @@ def create_approval(request: Request) -> Response:
if sum(1 for e in evidence if isinstance(e, dict) and e.get("kind") == "tool_call") > 1:
raise Invalid("an approval names exactly one tool_call; ask once per side effect (SO-37)")
decision = resolve_request(policy, capability, external)
base_decision = resolve_request({**policy, "grants": []}, capability, external)
if decision == "allow" and base_decision != "allow" and bound_conversation != conversation_id:
decision = "ask"
stamp = now()
record: dict[str, Any] = {
"schema": "hux.approval.v1", "id": new_id("apr"), "run_id": body.get("run_id"), "conversation_id": conversation_id,
@ -371,4 +380,3 @@ def register(router: Router) -> None:
router.add("GET", "/hux/v1/approvals/{id}", CARD, "approvals.read", get_approval)
router.add("POST", "/hux/v1/approvals/{id}", CARD, "approvals.decide", decide_approval)
budgets.register_routes(router)

View File

@ -81,7 +81,13 @@ DETAIL_ALLOWLIST: dict[str, frozenset[str]] = {
def canaries(environ: dict[str, str] | None = None) -> list[str]:
"""Literal secrets the pod was started with (SO-07); every one is scrubbed wherever it appears."""
environ = os.environ if environ is None else environ
values = [environ.get(name, "") for name in ("HUX_RELAY_KEY", "HUX_WORKER_KEY")]
names = ("HUX_ROUTER_KEY", "HUX_RELAY_KEY", "HUX_WORKER_KEY")
values = [environ.get(name, "") for name in names]
for name in names:
key_file = environ.get(f"{name}_FILE", "")
if key_file and os.path.exists(key_file):
with open(key_file, encoding="utf-8", errors="replace") as handle:
values.append(handle.read(4097).strip())
path = environ.get("HUX_CANARY_FILE", "")
if path and os.path.exists(path):
with open(path, encoding="utf-8", errors="replace") as handle:

View File

@ -13,6 +13,7 @@ import http.client
import json
import threading
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Mapping
from typing import Any
@ -24,6 +25,49 @@ HEADER_TRUST = "X-Hux-Trust"
HEADER_KEY = "X-Hux-Relay-Key"
DEFAULT_BASE_URL = "http://127.0.0.1:8790"
MESSAGE_MAX = 280
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1"})
class _RejectRedirect(urllib.request.HTTPRedirectHandler):
"""Never replay tenant identity or worker credentials to a redirect target."""
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001, ANN201, ARG002
"""Refuse every redirect rather than replaying the original headers."""
return None
def _validated_base_url(raw: str) -> str:
"""Return a canonical literal-loopback HTTP origin or reject it."""
parts = urllib.parse.urlsplit(raw)
if (
parts.scheme != "http"
or parts.hostname not in LOOPBACK_HOSTS
or parts.username is not None
or parts.password is not None
or parts.query
or parts.fragment
or parts.path not in {"", "/"}
or parts.port is None
):
raise ValueError("HUX base URL must be a literal loopback HTTP origin with an explicit port")
host = f"[{parts.hostname}]" if parts.hostname == "::1" else parts.hostname
return f"http://{host}:{parts.port}"
def _validated_path(path: str) -> str:
"""Accept only canonical relative HUX API paths owned by this client."""
if not isinstance(path, str):
raise HuxServiceError(400, "invalid", "malformed request path")
decoded = urllib.parse.unquote(path)
if (
not path.startswith("/hux/v1/")
or path.startswith("//")
or any(char in path for char in "?#\\\r\n")
or any(segment in {".", ".."} for segment in decoded.split("/"))
):
raise HuxServiceError(400, "invalid", "malformed request path")
return path
class HuxServiceError(Exception):
@ -73,13 +117,16 @@ class HuxClient:
def __init__(self, base_url: str = DEFAULT_BASE_URL, identity: Mapping[str, str] | None = None,
key: str | None = None, timeout: float = 5) -> None:
identity = dict(identity or {})
self.base_url = base_url.rstrip("/")
self.base_url = _validated_base_url(base_url)
self.identity = {
"tenant_slot": str(identity.get("tenant_slot", "")), "subject": str(identity.get("subject", "")),
"surface": str(identity.get("surface", "worker")), "trust": str(identity.get("trust", "worker")),
}
self._key = key
self.timeout = timeout
if isinstance(timeout, bool) or not isinstance(timeout, int | float) or not 0.1 <= float(timeout) <= 30:
raise ValueError("timeout must be between 0.1 and 30 seconds")
self.timeout = float(timeout)
self._opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), _RejectRedirect())
self._capabilities: dict[str, Any] | None = None
self._guard = threading.Lock()
@ -111,15 +158,15 @@ class HuxClient:
if body is not None:
data = json.dumps(body, sort_keys=True, separators=(",", ":")).encode()
extra["Content-Type"] = "application/json"
url = self.base_url + path
url = self.base_url + _validated_path(path)
if query:
url += "?" + "&".join(f"{k}={urllib.request.quote(str(v), safe='')}" for k, v in query.items())
url += "?" + urllib.parse.urlencode({str(k): str(v) for k, v in query.items()})
req = urllib.request.Request(url, data=data, method=method, headers=self.headers(extra))
try:
with urllib.request.urlopen(req, timeout=self.timeout) as raw: # noqa: S310 - loopback only
response = HuxResponse(raw.status, _decode(raw.read()), dict(raw.headers.items()))
with self._opener.open(req, timeout=self.timeout) as raw: # noqa: S310 - validated literal loopback only
response = HuxResponse(raw.status, _decode(_bounded_read(raw)), dict(raw.headers.items()))
except urllib.error.HTTPError as error:
payload = _decode(error.read())
payload = _decode(_bounded_read(error))
raise _error_from(error.code, payload) from None
except (urllib.error.URLError, OSError, TimeoutError) as error:
raise HuxUnavailable(f"hux service unreachable: {type(error).__name__}") from None
@ -176,3 +223,11 @@ def _decode(raw: bytes) -> Any:
return json.loads(raw)
except ValueError:
return None
def _bounded_read(raw: Any) -> bytes:
"""Read one bounded response so a compromised sidecar cannot exhaust the worker."""
data = raw.read(MAX_RESPONSE_BYTES + 1)
if len(data) > MAX_RESPONSE_BYTES:
raise HuxUnavailable("hux service returned an oversized response")
return data

View File

@ -72,6 +72,12 @@ def before_tool(client: HuxClient, run_id: str, conversation_id: str, tool_name:
return Decision(False, None, "invalid_run_id")
if not client.card_enabled(CARD_AUTONOMY):
return Decision(False, None, "hux_unavailable" if not client.capabilities()["reachable"] else "autonomy_off")
try:
# A zero-spend write pins the trusted worker's run -> conversation
# binding before a scoped session grant can create a new approval.
client.post(f"/hux/v1/runs/{run_id}/budget", {"conversation_id": conversation_id})
except HuxServiceError as error:
return Decision(False, None, _failure_reason(error))
argument_hash = canonical_argument_hash(tool_name, arguments)
size = len(canonical_json(arguments))
body = {
@ -182,12 +188,6 @@ def memory_gate(client: HuxClient, conversation_id: str) -> bool:
"""
try:
state = client.get(f"/hux/v1/conversations/{conversation_id}/privacy").body
except HuxServiceError as error:
if error.status != 404:
return False
try:
client.get("/hux/v1/privacy/policy")
except HuxServiceError:
return False
return True
except HuxServiceError:
return False
return bool(isinstance(state, dict) and state.get("memory_writes_allowed"))

View File

@ -104,8 +104,17 @@
"scripts/ops/hermes_handoff_rules.py",
"scripts/ops/hermes_handoff_run.py",
"testing/quality_handoff_mutation.py",
"services/hermes/scripts/hux_contracts.py",
"services/hermes/scripts/hux_policy.py"
"dockerfiles/hermes-hux-foundation/hux/budgets.py",
"dockerfiles/hermes-hux-foundation/hux/contracts.py",
"dockerfiles/hermes-hux-foundation/hux/errors.py",
"dockerfiles/hermes-hux-foundation/hux/flags.py",
"dockerfiles/hermes-hux-foundation/hux/http.py",
"dockerfiles/hermes-hux-foundation/hux/identity.py",
"dockerfiles/hermes-hux-foundation/hux/policy.py",
"dockerfiles/hermes-hux-foundation/hux/redaction.py",
"dockerfiles/hermes-hux-foundation/hux/rules.py",
"dockerfiles/hermes-worker-hux/hux_hook/client.py",
"dockerfiles/hermes-worker-hux/hux_hook/hooks.py"
],
"lint_paths": [
"ci/scripts/hermes_image_release.py",
@ -179,7 +188,18 @@
"scripts/ops/hermes_handoff_policy.py",
"scripts/ops/hermes_handoff_redaction.py",
"scripts/ops/hermes_handoff_rules.py",
"scripts/ops/hermes_handoff_run.py"
"scripts/ops/hermes_handoff_run.py",
"dockerfiles/hermes-hux-foundation/hux/budgets.py",
"dockerfiles/hermes-hux-foundation/hux/contracts.py",
"dockerfiles/hermes-hux-foundation/hux/errors.py",
"dockerfiles/hermes-hux-foundation/hux/flags.py",
"dockerfiles/hermes-hux-foundation/hux/http.py",
"dockerfiles/hermes-hux-foundation/hux/identity.py",
"dockerfiles/hermes-hux-foundation/hux/policy.py",
"dockerfiles/hermes-hux-foundation/hux/redaction.py",
"dockerfiles/hermes-hux-foundation/hux/rules.py",
"dockerfiles/hermes-worker-hux/hux_hook/client.py",
"dockerfiles/hermes-worker-hux/hux_hook/hooks.py"
],
"pytest_suites": {
"unit": {
@ -280,7 +300,8 @@
"services/hermes/scripts/stage_runtime_access.py",
"services/hermes/scripts/node_polkit_audit.py",
"scripts/ops/hermes_handoff_*.py",
"services/hermes/scripts/hux_*.py"
"dockerfiles/hermes-hux-foundation/hux/*.py",
"dockerfiles/hermes-worker-hux/hux_hook/*.py"
],
"naming_rules": [
{
@ -343,7 +364,18 @@
"scripts/ops/hermes_handoff_run.py",
"ci/scripts/semgrep_report.py",
"testing/quality_coverage.py",
"testing/quality_handoff_mutation.py"
"testing/quality_handoff_mutation.py",
"dockerfiles/hermes-hux-foundation/hux/budgets.py",
"dockerfiles/hermes-hux-foundation/hux/contracts.py",
"dockerfiles/hermes-hux-foundation/hux/errors.py",
"dockerfiles/hermes-hux-foundation/hux/flags.py",
"dockerfiles/hermes-hux-foundation/hux/http.py",
"dockerfiles/hermes-hux-foundation/hux/identity.py",
"dockerfiles/hermes-hux-foundation/hux/policy.py",
"dockerfiles/hermes-hux-foundation/hux/redaction.py",
"dockerfiles/hermes-hux-foundation/hux/rules.py",
"dockerfiles/hermes-worker-hux/hux_hook/client.py",
"dockerfiles/hermes-worker-hux/hux_hook/hooks.py"
],
"tracked_files": [
"services/hermes/scripts/voice_route_preflight.py",
@ -427,7 +459,18 @@
"scripts/ops/hermes_handoff_redaction.py",
"scripts/ops/hermes_handoff_rules.py",
"scripts/ops/hermes_handoff_run.py",
"testing/quality_handoff_mutation.py"
"testing/quality_handoff_mutation.py",
"dockerfiles/hermes-hux-foundation/hux/budgets.py",
"dockerfiles/hermes-hux-foundation/hux/contracts.py",
"dockerfiles/hermes-hux-foundation/hux/errors.py",
"dockerfiles/hermes-hux-foundation/hux/flags.py",
"dockerfiles/hermes-hux-foundation/hux/http.py",
"dockerfiles/hermes-hux-foundation/hux/identity.py",
"dockerfiles/hermes-hux-foundation/hux/policy.py",
"dockerfiles/hermes-hux-foundation/hux/redaction.py",
"dockerfiles/hermes-hux-foundation/hux/rules.py",
"dockerfiles/hermes-worker-hux/hux_hook/client.py",
"dockerfiles/hermes-worker-hux/hux_hook/hooks.py"
]
}
}

View File

@ -23,14 +23,14 @@ from hux import artifacts, audit, contracts, errors, identity, store # noqa: E4
from hux.server import build_router # noqa: E402
SCHEMAS = contracts.load_all()
OWNER = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat"}
OWNER = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
OTHER = {**OWNER, "X-Hux-Subject": "usr_fedcba9876543210"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, body=None, headers=OWNER):
@ -105,7 +105,7 @@ def test_contract_validation_guards_every_write(router, artifact):
def test_flag_off_hides_the_card(tmp_path, artifact):
router = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation"})
router = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation", "HUX_ROUTER_KEY": "rk"})
status, error = call(router, "POST", "/hux/v1/artifacts", {"type": "code", "title": "t", "content": "c"})
assert (status, error["code"]) == (404, "flag_off")
status, error = call(router, "GET", "/hux/v1/artifacts")
@ -142,5 +142,5 @@ def test_secret_bearing_content_is_kept_but_forced_restricted(router):
status, bumped = call(router, "POST", f"/hux/v1/artifacts/{clean['id']}/versions", {"content": f"key: {token}"}, {**OWNER, "If-Match": "1"})
assert status == 201 and bumped["sensitivity"] == "restricted"
from hux import audit, store
reasons = [r.get("reason") for r in audit.recent(store.TenantStore(router.data_root, identity.resolve(OWNER, {}))) if r["action"].startswith("artifacts.")]
reasons = [r.get("reason") for r in audit.recent(store.TenantStore(router.data_root, identity.resolve(OWNER, {"HUX_ROUTER_KEY": "rk"}))) if r["action"].startswith("artifacts.")]
assert reasons.count("secret_pattern_restricted") == 2

View File

@ -28,7 +28,7 @@ from hux import artifacts, contracts, diffs, errors, identity, store # noqa: E4
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"}
HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
@ -38,7 +38,7 @@ def ident() -> identity.Identity:
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, body=None, headers=None, raw=None):
@ -102,7 +102,7 @@ def test_create_accepts_base64_and_verifies_claimed_hash(router):
def test_worker_trust_is_recorded_as_system_actor(tmp_path):
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_WORKER_KEY": "wk"})
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk"})
headers = {"X-Hux-Trust": "worker", "X-Hux-Surface": "worker", "X-Hux-Relay-Key": "wk"}
status, record, _ = call(router, "POST", "/hux/v1/artifacts", {"type": "code", "title": "gen.py", "content": "print(1)\n"}, headers)
assert status == 201 and record["versions"][0]["created_by"] == {"type": "system", "id": "worker"}

View File

@ -23,7 +23,7 @@ from hux import audit, contracts, errors, events, identity, redaction, store #
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"}
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"])
CONV = "conv_0001abcd"
@ -35,7 +35,7 @@ def ident(**overrides) -> identity.Identity:
def router_for(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, headers=HEADERS, body=None):
@ -357,7 +357,7 @@ def test_stream_redacts_for_voice_surface(tmp_path):
def test_flag_off_hides_event_routes(tmp_path):
router = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation"})
router = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation", "HUX_ROUTER_KEY": "rk"})
assert call(router, "GET", f"/hux/v1/conversations/{CONV}/events")[1]["code"] == "flag_off"

View File

@ -27,7 +27,7 @@ from hux.http import Router, Response, page, serve # 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"}
HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
@ -39,7 +39,7 @@ def ident(**overrides) -> identity.Identity:
# --- identity -------------------------------------------------------------------
def test_identity_from_router_headers():
who = identity.resolve(HEADERS, {})
who = identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})
assert who == ident()
assert contracts.validate("common.schema.json", who.record(), SCHEMAS, "/$defs/identity") == []
@ -50,7 +50,7 @@ def test_identity_from_router_headers():
])
def test_identity_rejects_bad_headers(bad):
with pytest.raises(errors.Unauthorized):
identity.resolve({**HEADERS, **bad}, {})
identity.resolve({**HEADERS, **bad}, {"HUX_ROUTER_KEY": "rk"})
def test_relay_and_worker_need_their_keys():
@ -62,13 +62,48 @@ def test_relay_and_worker_need_their_keys():
assert identity.resolve({**relay, "X-Hux-Relay-Key": "secret"}, {"HUX_RELAY_KEY": "secret"}).trust == "relay"
worker = {**HEADERS, "X-Hux-Trust": "worker", "X-Hux-Surface": "worker", "X-Hux-Relay-Key": "wk"}
assert identity.resolve(worker, {"HUX_WORKER_KEY": "wk"}).surface == "worker"
assert identity.resolve(HEADERS).trust == "router"
assert identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}).trust == "router"
def test_router_key_is_required_and_secret_file_is_supported(tmp_path):
"""Same-pod callers cannot forge router trust; a projected 0400 secret file authenticates it."""
bare = {key: value for key, value in HEADERS.items() if key != "X-Hux-Relay-Key"}
with pytest.raises(errors.Unauthorized) as missing:
identity.resolve(bare, {"HUX_ROUTER_KEY": "router-secret"})
assert "router-secret" not in str(missing.value)
with pytest.raises(errors.Unauthorized):
identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "router-secret", "HUX_RELAY_KEY": "rk"})
key_file = tmp_path / "router-key"
key_file.write_text("router-secret\n")
key_file.chmod(0o400)
authenticated = {**bare, "X-Hux-Relay-Key": "router-secret"}
assert identity.resolve(authenticated, {"HUX_ROUTER_KEY_FILE": str(key_file)}).trust == "router"
router = build_router(tmp_path / "data", {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "router-secret"})
assert router.dispatch("GET", "/hux/v1/capabilities", bare, b"").status == 401
assert not (tmp_path / "data" / "hux").exists()
@pytest.mark.parametrize("payload", [None, b"x" * (identity.MAX_KEY_BYTES + 1), b"\xff"])
def test_router_key_file_failures_are_unauthorized(tmp_path, payload):
"""Missing, oversized, and non-UTF-8 projected secrets fail closed without exceptions or values."""
key_file = tmp_path / "router-key"
if payload is not None:
key_file.write_bytes(payload)
with pytest.raises(errors.Unauthorized) as denied:
identity.resolve(HEADERS, {"HUX_ROUTER_KEY_FILE": str(key_file)})
assert "x" * 32 not in str(denied.value) and "\\xff" not in str(denied.value)
def test_router_trust_cannot_claim_worker_surface():
"""Even an authenticated BFF key cannot impersonate the separately keyed worker hop."""
with pytest.raises(errors.Unauthorized, match="worker surface"):
identity.resolve({**HEADERS, "X-Hux-Surface": "worker"}, {"HUX_ROUTER_KEY": "rk"})
def test_slot_must_match_the_pod_it_reaches():
with pytest.raises(errors.Unauthorized):
identity.resolve(HEADERS, {"HUX_TENANT_SLOT": "slot-4"})
assert identity.resolve(HEADERS, {"HUX_TENANT_SLOT": "slot-3"}).tenant_slot == "slot-3"
identity.resolve(HEADERS, {"HUX_TENANT_SLOT": "slot-4", "HUX_ROUTER_KEY": "rk"})
assert identity.resolve(HEADERS, {"HUX_TENANT_SLOT": "slot-3", "HUX_ROUTER_KEY": "rk"}).tenant_slot == "slot-3"
def test_server_refuses_non_loopback_bind():
@ -90,7 +125,9 @@ def test_flags_fail_closed_and_capabilities_validate():
on = flags.Flags({"HUX_FLAGS": ALL_ON})
record = on.capabilities(ident(), {"commit": "d3cbeb06" * 5, "image_digest": "sha256:" + "4a" * 32, "junk": "x"})
assert contracts.validate_record(record, SCHEMAS) == []
assert all(card["enabled"] for card in record["cards"])
enabled = {card["card"]: card["enabled"] for card in record["cards"]}
assert all(enabled[card] for card, routes in flags.CARD_ROUTES.items() if routes)
assert all(not enabled[card] for card, routes in flags.CARD_ROUTES.items() if not routes)
assert {card["card"] for card in record["cards"]} == {f"HUX-{n:02d}" for n in range(1, 13)}
assert not on.enabled("HUX-99")
assert flags.build_from_environ({"HUX_BUILD_COMMIT": "abc"}) == {"commit": "abc", "image_digest": ""}
@ -224,7 +261,7 @@ def test_audit_rows_validate_and_never_carry_bodies(tmp_path):
# --- http pipeline ---------------------------------------------------------------
def _router(tmp_path, flags_value=ALL_ON, environ=None) -> Router:
env = {"HUX_FLAGS": flags_value, **(environ or {})}
env = {"HUX_FLAGS": flags_value, "HUX_ROUTER_KEY": "rk", **(environ or {})}
return build_router(tmp_path, env)
@ -323,6 +360,7 @@ def test_real_server_serves_json_and_sse(tmp_path):
conn.request("GET", "/hux/v1/s", headers=HEADERS)
reply = conn.getresponse()
assert reply.getheader("Content-Type") == "text/event-stream"
assert reply.getheader("Cache-Control") == "no-store"
assert reply.read() == b"id: 1\ndata: {}\n\nid: 2\ndata: {}\n\n"
finally:
server.shutdown()
@ -333,6 +371,8 @@ def test_all_errors_serialise_to_contract():
for cls in (errors.Unauthorized, errors.Forbidden, errors.NotFound, errors.FlagOff, errors.Conflict, errors.Invalid, errors.TooLarge, errors.ApprovalRequired, errors.BudgetExhausted):
record = cls("m" * 300, ["d" * 300] * 40).record()
assert contracts.validate_record(record, SCHEMAS) == [], cls
limited = errors.RateLimited(7)
assert limited.retry_after == 7 and contracts.validate_record(limited.record(), SCHEMAS) == []
def test_foundation_sources_stay_under_500_lines():
@ -389,13 +429,16 @@ def test_worker_trust_reaches_only_the_allowlisted_routes(tmp_path):
off = _router(tmp_path, flags_value="", environ={"HUX_WORKER_KEY": "wk"})
assert _call(off, "GET", "/hux/v1/approvals", WORKER)[0] == 403, "the allowlist answers before the flag does"
assert _call(off, "GET", "/hux/v1/capabilities", HEADERS)[0] == 404, "humans still see flag-off as not found"
assert flags.worker_may_call("GET", "/hux/v1/releases") and not flags.worker_may_call("GET", "/hux/v1/policy")
assert not flags.worker_may_call("GET", "/hux/v1/releases") and not flags.worker_may_call("GET", "/hux/v1/policy")
assert all(any(t == route.template for _, t in flags.WORKER_ROUTES if route.method == _) or (route.method, route.template) not in flags.WORKER_ROUTES for route in router.routes)
def test_unshipped_cards_declare_no_routes():
"""F12 (low): HUX-06, HUX-09 and HUX-12 own no routes until they ship, so capabilities never advertises a 404."""
assert flags.CARD_ROUTES["HUX-06"] == flags.CARD_ROUTES["HUX-09"] == flags.CARD_ROUTES["HUX-12"] == []
def test_unshipped_cards_declare_no_routes_and_cannot_enable():
"""F12: cards without a server implementation stay disabled even when every raw flag is configured."""
unshipped = {"HUX-06", "HUX-07", "HUX-09", "HUX-12"}
configured = flags.Flags({"HUX_FLAGS": ALL_ON})
assert all(flags.CARD_ROUTES[card] == [] for card in unshipped)
assert all(not configured.enabled(card) for card in unshipped)
def test_unexpected_handler_exceptions_become_a_500_error_record(tmp_path):

View File

@ -45,7 +45,7 @@ CANARY = "CANARY-9c1d-SECRET"
def start(tmp_path: Path, flags: str = ALL_ON):
router = build_router(tmp_path, {"HUX_FLAGS": flags, "HUX_WORKER_KEY": "wk"})
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
@ -75,7 +75,7 @@ def test_headers_match_the_service_vocabulary():
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)
human = HuxClient(base, HUMAN, key="rk")
with pytest.raises(HuxServiceError) as bad:
human.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)
@ -96,7 +96,7 @@ def test_transport_edge_cases(monkeypatch):
def raise_http(*args, **kwargs):
raise urllib.error.HTTPError("u", 502, "bad gateway", {}, io.BytesIO(b"<html>"))
monkeypatch.setattr(urllib.request, "urlopen", raise_http)
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")
@ -105,7 +105,7 @@ def test_transport_edge_cases(monkeypatch):
status = 418
headers = {"X-Test": "1"}
def read(self):
def read(self, *_args):
return b"{bad json"
def __enter__(self):
@ -114,7 +114,7 @@ def test_transport_edge_cases(monkeypatch):
def __exit__(self, *exc):
return False
monkeypatch.setattr(urllib.request, "urlopen", lambda *a, **k: Raw())
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
@ -122,22 +122,62 @@ def test_transport_edge_cases(monkeypatch):
def raise_socket(*args, **kwargs):
raise TimeoutError("slow")
monkeypatch.setattr(urllib.request, "urlopen", raise_socket)
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(urllib.request, "urlopen", raise_half_closed)
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)
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)
@ -176,7 +216,7 @@ def test_capabilities_cached_per_process(live, monkeypatch):
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)
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},
@ -187,7 +227,7 @@ def test_emit_is_best_effort_and_redaction_safe(live):
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), conv, "tool.call", "cross tenant") 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())
@ -198,13 +238,13 @@ def test_emit_is_best_effort_and_redaction_safe(live):
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)
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 True
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

View File

@ -24,14 +24,14 @@ from hux import audit, contracts, identity, organization, 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"}
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"])
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, body=None, headers=HEADERS):
@ -90,7 +90,7 @@ def test_project_patch_needs_if_match_and_audits_unconditional_writes(router, tm
assert call(router, "PATCH", path, {"name": ""}, {**HEADERS, "If-Match": "3"})[0] == 400
assert call(router, "PATCH", path, {"default_mode": "turbo"}, {**HEADERS, "If-Match": "3"})[0] == 400
assert call(router, "PATCH", path, [], {**HEADERS, "If-Match": "3"})[0] == 400
rows = [(r["action"], r["outcome"], r.get("reason", "")) for r in audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {}))) if r["action"] == "projects.update"]
rows = [(r["action"], r["outcome"], r.get("reason", "")) for r in audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))) if r["action"] == "projects.update"]
assert rows[0] == ("projects.update", "allow", "") and rows[1][1] == "conflict" and rows[2] == ("projects.update", "allow", "unconditional_write")
@ -147,8 +147,8 @@ def test_conversation_create_list_filters_and_patch(router):
def test_helpers_for_other_lanes(router, tmp_path):
project = make_project(router)
conversation = make_conversation(router, project_id=project["id"])
mine = store.TenantStore(tmp_path, identity.resolve(HEADERS, {}))
theirs = store.TenantStore(tmp_path, identity.resolve(OTHER, {}))
mine = store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))
theirs = store.TenantStore(tmp_path, identity.resolve(OTHER, {"HUX_ROUTER_KEY": "rk"}))
assert organization.project_exists(mine, project["id"]) and not organization.project_exists(theirs, project["id"])
assert organization.conversation_exists(mine, conversation["id"]) and not organization.conversation_exists(theirs, conversation["id"])
assert not organization.project_exists(mine, "../escape") and not organization.conversation_exists(mine, None)
@ -181,7 +181,7 @@ def test_branch_copies_project_tags_and_mode_and_lineage_walks_both_ways(router)
def test_lineage_survives_a_missing_or_cyclic_parent(router, tmp_path):
orphan = make_conversation(router)
mine = store.TenantStore(tmp_path, identity.resolve(HEADERS, {}))
mine = store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))
mine.put("conversations", {**orphan, "branch": {"parent_conversation_id": "conv_gone00000001", "branch_point_message_id": "m"}})
assert call(router, "GET", f"/hux/v1/conversations/{orphan['id']}/lineage")[1]["ancestors"] == []
mine.put("conversations", {**mine.get("conversations", orphan["id"]), "branch": {"parent_conversation_id": orphan["id"], "branch_point_message_id": "m"}})
@ -220,7 +220,7 @@ def test_f13c_search_finds_conversations_by_artifact_title(router, monkeypatch):
assert status == 201, artifact
status, body, _ = call(router, "GET", "/hux/v1/search?q=supplier")
assert [c["id"] for c in body["items"]] == [conversation["id"]] and body["scores"][conversation["id"]] == 1
s = store.TenantStore(router.data_root, identity.resolve(HEADERS, {}))
s = store.TenantStore(router.data_root, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))
assert organization.artifact_titles(s, conversation) == ["Supplier comparison sheet"] and organization.artifact_titles(s, other) == []
# The conversation's own artifact_ids list is indexed as well, without duplicates.
s.put(organization.CONVERSATIONS, {**s.get(organization.CONVERSATIONS, other["id"]), "artifact_ids": [artifact["id"], "art_missing0001"]}, 1)
@ -249,6 +249,6 @@ def test_f9_titles_tags_names_and_descriptions_are_secret_scrubbed(router):
def test_flag_off_hides_the_card(tmp_path):
off = build_router(tmp_path, {"HUX_FLAGS": ""})
off = build_router(tmp_path, {"HUX_FLAGS": "", "HUX_ROUTER_KEY": "rk"})
status, body, _ = call(off, "GET", "/hux/v1/projects")
assert (status, body["code"]) == (404, "flag_off")

View File

@ -0,0 +1,169 @@
"""Adversarial HTTP boundary tests for the loopback HUX foundation service."""
from __future__ import annotations
import json
import socket
import sys
import threading
from http.client import HTTPConnection
from pathlib import Path
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 # noqa: E402
from hux import http as hux_http # noqa: E402
from hux.http import DEFAULT_REQUEST_TIMEOUT_SECONDS, RateLimiter, Router, serve # noqa: E402
from hux.server import build_router # noqa: E402
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
HEADERS = {
"X-Hermes-Tenant-Identity": "slot-3",
"X-Hux-Subject": "usr_0123456789abcdef",
"X-Hux-Surface": "chat",
"X-Hux-Relay-Key": "rk",
}
def _router(tmp_path: Path, **extra: str) -> Router:
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", **extra})
def _start(router: Router):
server = serve(router, "127.0.0.1", 0)
threading.Thread(target=server.serve_forever, daemon=True).start()
return server
def _raw_request(address: tuple[str, int], request: bytes) -> bytes:
"""Send one HTTP/1.0 request and return its complete close-delimited response."""
with socket.create_connection(address, timeout=1) as client:
client.sendall(request)
chunks = []
while chunk := client.recv(4096):
chunks.append(chunk)
return b"".join(chunks)
def test_declared_body_is_rejected_before_socket_read(tmp_path):
"""An oversized Content-Length receives 413 without waiting for the claimed body."""
server = _start(_router(tmp_path))
try:
conn = HTTPConnection("127.0.0.1", server.server_address[1], timeout=1)
conn.putrequest("POST", "/hux/v1/capabilities")
conn.putheader("Content-Length", str(1024 * 1024 + 1))
conn.endheaders()
reply = conn.getresponse()
body = json.loads(reply.read())
assert (reply.status, body["code"]) == (413, "too_large")
assert reply.getheader("Cache-Control") == "no-store"
finally:
server.shutdown()
server.server_close()
def test_incomplete_body_times_out_and_transfer_encoding_is_rejected(tmp_path):
"""Accepted sockets have a deadline, and unsupported framing fails closed."""
server = _start(_router(tmp_path, HUX_REQUEST_TIMEOUT_SECONDS="0.1"))
try:
reply = _raw_request(
server.server_address,
b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: 2\r\n\r\n{",
)
assert b" 400 " in reply and b"request body is incomplete or timed out" in reply
conn = HTTPConnection("127.0.0.1", server.server_address[1], timeout=1)
conn.putrequest("POST", "/hux/v1/capabilities")
conn.putheader("Transfer-Encoding", "chunked")
conn.endheaders()
assert conn.getresponse().status == 400
finally:
server.shutdown()
server.server_close()
def test_malformed_duplicate_and_absurd_content_lengths_fail_closed(tmp_path):
"""Ambiguous or computationally large length headers never reach ``int`` or a body allocation."""
server = _start(_router(tmp_path))
try:
malformed = _raw_request(
server.server_address,
b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: nope\r\n\r\n",
)
duplicate = _raw_request(
server.server_address,
b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n",
)
absurd = _raw_request(
server.server_address,
b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: 99999999999\r\n\r\n",
)
assert b" 400 " in malformed and b" 400 " in duplicate
assert b" 413 " in absurd
finally:
server.shutdown()
server.server_close()
def test_rate_limit_is_per_subject_and_method_class(tmp_path):
"""Read and write buckets are distinct and return a bounded Retry-After."""
router = _router(tmp_path, HUX_READS_PER_MINUTE="1", HUX_WRITES_PER_MINUTE="1")
first = router.dispatch("GET", "/hux/v1/capabilities", HEADERS, b"")
limited = router.dispatch("GET", "/hux/v1/capabilities", HEADERS, b"")
write = router.dispatch("POST", "/hux/v1/capabilities", HEADERS, b"{}")
assert first.status == 200
assert (limited.status, limited.body["code"]) == (429, "rate_limited")
assert 1 <= int(limited.headers["Retry-After"]) <= 60
assert write.status == 405, "method routing precedes the write rate bucket"
live = _start(_router(tmp_path / "live", HUX_READS_PER_MINUTE="1"))
try:
conn = HTTPConnection("127.0.0.1", live.server_address[1], timeout=1)
conn.request("GET", "/hux/v1/capabilities", headers=HEADERS)
conn.getresponse().read()
conn.request("GET", "/hux/v1/capabilities", headers=HEADERS)
reply = conn.getresponse()
reply.read()
assert reply.status == 429 and reply.getheader("Retry-After")
assert reply.getheader("Cache-Control") == "no-store"
finally:
live.shutdown()
live.server_close()
def test_limiter_expires_windows_and_invalid_settings_use_safe_defaults(tmp_path, monkeypatch):
"""Expired subjects are evicted and malformed operator settings cannot disable bounds."""
moments = iter((0.0, 0.0, 61.0))
limiter = RateLimiter(1, 1, clock=lambda: next(moments))
assert limiter.check("a", "GET") is None
assert limiter.check("a", "GET") == 60
assert limiter.check("a", "GET") is None
monkeypatch.setattr(hux_http, "MAX_RATE_BUCKETS", 1)
bounded = RateLimiter(2, 2, clock=lambda: 0.0)
assert bounded.check("a", "GET") is None
assert bounded.check("b", "GET") == 60
router = _router(
tmp_path,
HUX_READS_PER_MINUTE="nan",
HUX_WRITES_PER_MINUTE="0",
HUX_REQUEST_TIMEOUT_SECONDS="forever",
)
assert router.rate_limiter.limits == {"read": 300, "write": 30}
assert router.request_timeout == DEFAULT_REQUEST_TIMEOUT_SECONDS
def test_health_and_json_are_no_store_and_health_refuses_bodies(tmp_path):
"""Sensitive JSON never enters browser caches; health cannot be used for body smuggling."""
server = _start(_router(tmp_path))
try:
conn = HTTPConnection("127.0.0.1", server.server_address[1], timeout=1)
conn.request("GET", "/healthz")
reply = conn.getresponse()
reply.read()
assert reply.getheader("Cache-Control") == "no-store"
conn.request("GET", "/healthz", body=b"x", headers={"Content-Length": "1"})
assert conn.getresponse().status == 413
finally:
server.shutdown()
server.server_close()

View File

@ -24,7 +24,7 @@ from hux import audit, contracts, events, identity, memory, 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"}
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"])
CONV = "conv_0001abcd"
@ -37,7 +37,7 @@ def ident(**overrides) -> identity.Identity:
def router_for(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, headers=HEADERS, body=None):
@ -256,7 +256,7 @@ def test_retrieval_removal_and_restore(tmp_path):
def test_decisions_need_a_human_surface(tmp_path):
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_WORKER_KEY": "wk"})
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk"})
_, proposed, _ = propose(router)
worker = {**HEADERS, "X-Hux-Surface": "worker", "X-Hux-Trust": "worker", "X-Hux-Relay-Key": "wk"}
status, body, _ = call(router, "POST", f"/hux/v1/memory/{proposed['id']}/approve", worker)

View File

@ -23,7 +23,7 @@ from hux.errors import Conflict # 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"}
HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
USER = {"proposed_by": "user", "approval_mode": "automatic"}
@ -33,7 +33,7 @@ def ident() -> identity.Identity:
def router_for(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, body=None, headers=None):

View File

@ -21,7 +21,7 @@ from hux import contracts, events, identity, memory, privacy, store # noqa: E40
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"}
HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
CONV = "conv_0001abcd"
@ -31,7 +31,7 @@ def ident() -> identity.Identity:
def router_for(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, headers=HEADERS, body=None):

View File

@ -29,7 +29,7 @@ from hux import events as hux_events # 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"}
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"}
WORKER = {**HEADERS, "X-Hux-Surface": "worker", "X-Hux-Trust": "worker", "X-Hux-Relay-Key": "wk"}
RELAY = {**HEADERS, "X-Hux-Surface": "telegram", "X-Hux-Trust": "relay", "X-Hux-Relay-Key": "rk"}
@ -42,7 +42,7 @@ OTHER_HASH = "sha256:" + "cd" * 32
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_WORKER_KEY": "wk", "HUX_RELAY_KEY": "rk"})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk", "HUX_RELAY_KEY": "rk"})
@pytest.fixture
@ -152,7 +152,7 @@ def test_only_humans_on_router_or_relay_decide(router, tmp_path):
status, body = call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "once"}, RELAY)
assert status == 200 and valid(body)["status"] == "approved"
assert body["decision"]["by"] == {"type": "user", "id": HEADERS["X-Hux-Subject"]}
rows = audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {})))
rows = audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})))
assert [(r["action"], r["outcome"]) for r in rows if r["action"] == "approvals.decide"] == [("approvals.decide", "deny")] * 2 + [("approvals.decide", "allow")]
@ -233,7 +233,7 @@ def test_exhausted_budget_blocks_new_approvals(router, events):
assert (status, error["code"]) == (429, "budget_exhausted") and error["details"] == ["tool_calls_per_run"]
assert [e["kind"] for e in events] == ["budget.exhausted", "budget.exhausted"]
status, body = call(router, "POST", "/hux/v1/approvals", request_body("read_files", run_id="run_fresh"), WORKER)
assert status == 201, "another run keeps its own budget"
assert (status, body["code"]) == (429, "budget_exhausted"), "run rotation cannot reset conversation spend"
# --- gate --------------------------------------------------------------------------
@ -260,7 +260,7 @@ def test_gate_blocks_before_approval_and_releases_once_exactly_once(router, even
assert released["evidence"] == [{"kind": "approval", "id": record["id"]}] and released["run_id"] == "run_9f"
served = call(router, "GET", f"/hux/v1/approvals/{record['id']}")[1]
assert valid(served) and "_consumed_at" not in served
rows = audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {})))
rows = audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})))
assert [r["outcome"] for r in rows if r["action"] == "gate.check"] == ["deny", "deny", "deny", "deny", "allow", "deny"]
@ -269,10 +269,21 @@ def test_gate_session_is_reusable_within_the_conversation(router):
call(router, "POST", f"/hux/v1/approvals/{record['id']}", {"choice": "session"})
assert gate(router, capability="shell")[1]["proceed"] is True
assert gate(router, capability="shell", argument_hash=OTHER_HASH)[1]["proceed"] is True
assert gate(router, run_id="run_later", capability="shell")[1]["proceed"] is True, "unknown run: the approval's conversation applies (F4)"
assert gate(router, run_id="run_later", capability="shell")[1]["proceed"] is False, "unknown runs inherit no approval"
status, unbound = call(router, "POST", "/hux/v1/approvals", request_body("shell", run_id="run_unbound"), WORKER)
assert status == 201 and unbound["status"] == "pending", "a scoped grant needs a prior authoritative binding"
call(router, "POST", "/hux/v1/runs/run_later/budget", {"conversation_id": CONV}, WORKER)
assert gate(router, run_id="run_later", capability="shell")[1]["proceed"] is False, "the old approval record never spans runs"
status, fresh = call(router, "POST", "/hux/v1/approvals", request_body("shell", run_id="run_later"), WORKER)
assert status == 201 and fresh["status"] == "approved" and fresh["id"] != record["id"]
assert gate(router, run_id="run_later", capability="shell")[1]["approval_id"] == fresh["id"]
call(router, "POST", "/hux/v1/runs/run_elsewhere/budget", {"conversation_id": "conv_elsewhere01", "tokens": 1}, WORKER)
status, body = gate(router, run_id="run_elsewhere", capability="shell", conversation_id=CONV)
assert body["proceed"] is False, "the run's own conversation wins over the body's claim (F4)"
elsewhere = request_body("shell", run_id="run_elsewhere")
elsewhere["conversation_id"] = "conv_elsewhere01"
status, scoped = call(router, "POST", "/hux/v1/approvals", elsewhere, WORKER)
assert status == 201 and scoped["status"] == "pending", "a session grant never crosses conversations"
assert gate(router, capability="write_files")[1]["proceed"] is False
@ -351,9 +362,34 @@ def test_gate_refuses_when_the_run_budget_is_exhausted(router, events):
status, body = gate(router, capability="shell")
assert body == {"proceed": False, "reason": "budget_exhausted", "exhausted": ["tool_calls_per_run"]}
assert events[-1]["kind"] == "budget.exhausted" and events[-1]["run_id"] == "run_9f" and events[-1]["conversation_id"] == CONV
assert gate(router, run_id="run_fresh", capability="shell")[1]["proceed"] is True, "the budget is per run"
assert gate(router, run_id="run_fresh", capability="shell")[1]["proceed"] is False, "unknown runs inherit nothing"
call(router, "POST", "/hux/v1/runs/run_fresh/budget", {"conversation_id": CONV}, WORKER)
assert gate(router, run_id="run_fresh", capability="shell")[1]["reason"] == "budget_exhausted"
status, error = call(router, "POST", "/hux/v1/approvals", request_body("shell", run_id="run_fresh"), WORKER)
assert (status, error["code"]) == (429, "budget_exhausted")
call(router, "POST", "/hux/v1/runs/run_other/budget", {"conversation_id": "conv_other0001"}, WORKER)
assert gate(router, run_id="run_other", capability="shell")[1]["proceed"] is False
assert budgets.run_conversation(router_store(router), "run_nobody") is None
def test_conversation_budget_aggregates_across_runs_and_policy_revision_resets(router):
"""Run rotation cannot reset spend; an explicit human policy revision starts a new budget epoch."""
policy_body = {
"scope": {"level": "conversation", "scope_id": CONV},
"autonomy": "safe",
"budgets": {"tool_calls_per_run": 3},
}
assert call(router, "PUT", "/hux/v1/policy", policy_body)[0] == 200
first = call(router, "POST", "/hux/v1/runs/run_a/budget", {"conversation_id": CONV, "tool_calls": 2}, WORKER)[1]
second = call(router, "POST", "/hux/v1/runs/run_b/budget", {"conversation_id": CONV, "tool_calls": 1}, WORKER)[1]
assert first["spent"]["tool_calls"] == 2
assert second["spent"]["tool_calls"] == 3 and second["exhausted"] == ["tool_calls_per_run"]
assert gate(router, run_id="run_b", capability="shell")[1]["reason"] == "budget_exhausted"
revised = call(router, "PUT", "/hux/v1/policy", policy_body)[1]
assert revised["revision"] == 2
reset = call(router, "GET", "/hux/v1/runs/run_b/budget", headers=WORKER)[1]
assert reset["spent"]["tool_calls"] == 0 and reset["exhausted"] == []
def router_store(router):
return store.TenantStore(router.data_root, identity.resolve(HEADERS, {}))
return store.TenantStore(router.data_root, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))

View File

@ -42,7 +42,7 @@ RUN = "run_hook_1"
def start(tmp_path: Path, flags: str = ALL_ON) -> tuple[str, object]:
"""Run the real service on an ephemeral loopback port for one test."""
router = build_router(tmp_path, {"HUX_FLAGS": flags, "HUX_WORKER_KEY": "wk"})
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
@ -51,7 +51,7 @@ def start(tmp_path: Path, flags: str = ALL_ON) -> tuple[str, object]:
@pytest.fixture
def service(tmp_path):
base, server = start(tmp_path)
human = HuxClient(base, HUMAN)
human = HuxClient(base, HUMAN, key="rk")
conversation = human.post("/hux/v1/conversations", {"title": "hook test"}).body["id"]
yield {"base": base, "root": tmp_path, "agent": HuxClient(base, IDENTITY, key="wk"), "human": human, "conv": conversation}
server.shutdown()

View File

@ -24,7 +24,7 @@ from hux import audit, contracts, errors, identity, policy, rules, store # noqa
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"}
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)
@ -32,7 +32,7 @@ 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})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
@pytest.fixture
@ -65,7 +65,7 @@ def test_first_read_creates_the_safe_global_default(router, tmp_path):
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, {})))
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"]
@ -120,7 +120,7 @@ def test_put_honours_if_match_and_audits_unconditional_writes(router, tmp_path):
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, {})))]
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]
@ -176,7 +176,7 @@ def test_second_subject_sees_its_own_default_not_the_first_tenants_policy(router
def test_flag_off_hides_the_whole_card(tmp_path):
off = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation"})
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")
@ -195,7 +195,7 @@ def test_helpers_round_trip_time_and_actors():
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_WORKER_KEY": "wk", "HUX_RELAY_KEY": "rk"})
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"}
@ -209,7 +209,7 @@ def test_only_a_human_actor_may_write_policy_or_hold_allow_grants(tmp_path):
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, {}))) if r["action"] == "policy.write"]
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")

View File

@ -25,7 +25,7 @@ from hux import events as hux_events # 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"}
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"}
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"])
@ -34,7 +34,7 @@ CONV = "conv_0001abcd"
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_WORKER_KEY": "wk"})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk"})
@pytest.fixture
@ -112,7 +112,7 @@ def test_stop_writes_a_receipt_and_a_second_stop_returns_it(router, events, tmp_
assert (status, again) == (200, body)
assert [e["kind"] for e in events] == ["run.cancelled"]
assert events[0]["evidence"] == [{"kind": "run", "id": body["id"]}] and events[0]["run_id"] == "run_9f"
rows = [(r["action"], r.get("reason")) for r in audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {}))) if r["action"] == "runs.stop"]
rows = [(r["action"], r.get("reason")) for r in audit.recent(store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))) if r["action"] == "runs.stop"]
assert rows == [("runs.stop", "cancelled"), ("runs.stop", "replayed")]
@ -126,7 +126,7 @@ def test_only_the_gateway_may_vouch_for_an_empty_registry_and_a_failed_receipt_c
status, done = call(router, "POST", "/hux/v1/runs/run_9f/stop", {"process_registry_empty": True, "side_effects": [{"description": "x", "reverted": True}]})
assert status == 201 and valid(done)["outcome"] == "cancelled" and done["requested_at"] == first_requested and "completed_at" in done
assert done["conversation_id"] == CONV and done["requested_by"] == {"type": "system", "id": "worker"}
tenant = store.TenantStore(tmp_path, identity.resolve(HEADERS, {}))
tenant = store.TenantStore(tmp_path, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))
stored = tenant.get("receipts", done["id"])
assert stored["revision"] == 2 and contracts.validate_record(stored, SCHEMAS) == [], "stored receipts carry revision (F11)"
status, third = call(router, "POST", "/hux/v1/runs/run_9f/stop", {"process_registry_empty": False})

View File

@ -24,7 +24,7 @@ from hux import audit, contracts, errors, events, identity, memory, privacy, sto
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"}
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"])
CONV = "conv_0001abcd"
@ -35,7 +35,7 @@ def ident() -> identity.Identity:
def router_for(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, headers=HEADERS, body=None):
@ -117,7 +117,7 @@ def test_audit_route_lists_newest_first_and_is_tenant_scoped(tmp_path):
def test_retention_runs_without_flags(tmp_path):
router = build_router(tmp_path, {"HUX_FLAGS": ""})
router = build_router(tmp_path, {"HUX_FLAGS": "", "HUX_ROUTER_KEY": "rk"})
s = tenant(tmp_path)
assert call(router, "GET", "/hux/v1/privacy/audit")[0] == 404
assert counts(privacy.run_retention(s))["report"] == 1

View File

@ -23,7 +23,7 @@ from hux import audit, contracts, events, identity, privacy, rules, store # noq
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"}
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"])
CONV = "conv_0001abcd"
@ -34,7 +34,7 @@ def ident() -> identity.Identity:
def router_for(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, headers=HEADERS, body=None):
@ -165,7 +165,7 @@ def test_forget_unknown_or_foreign_conversation_is_404(tmp_path):
def test_flag_off_hides_privacy_routes(tmp_path):
router = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation"})
router = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation", "HUX_ROUTER_KEY": "rk"})
assert call(router, "GET", "/hux/v1/privacy/policy")[1]["code"] == "flag_off"

View File

@ -26,7 +26,7 @@ from hux import audit, contracts, identity, research, store # noqa: E402
from hux.server import build_router # noqa: E402
SCHEMAS = contracts.load_all()
OWNER = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat"}
OWNER = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
OTHER = {**OWNER, "X-Hux-Subject": "usr_fedcba9876543210"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
@ -37,7 +37,7 @@ def tenant(router, subject="usr_0123456789abcdef") -> store.TenantStore:
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, body=None, headers=None, raw=None):
@ -246,7 +246,7 @@ def test_research_modules_stay_under_500_lines():
def test_worker_trust_records_a_system_actor(tmp_path):
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_WORKER_KEY": "wk"})
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_WORKER_KEY": "wk"})
headers = {"X-Hux-Trust": "worker", "X-Hux-Surface": "worker", "X-Hux-Relay-Key": "wk"}
status, record, _ = call(router, "POST", "/hux/v1/sources", {"kind": "tool_output", "title": "grep"}, headers)
assert status == 201 and record["provenance"] == {"surface": "worker", "actor": {"type": "system", "id": "worker"}, "recorded_at": record["retrieved_at"]}

View File

@ -24,14 +24,14 @@ from hux import audit, contracts, identity, store # noqa: E402
from hux.server import build_router # noqa: E402
SCHEMAS = contracts.load_all()
OWNER = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat"}
OWNER = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
OTHER = {**OWNER, "X-Hux-Subject": "usr_fedcba9876543210"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON})
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, body=None, headers=None, raw=None):

View File

@ -33,6 +33,35 @@ def test_handoff_modules_are_exactly_managed_linted_and_covered():
assert "scripts/ops/hermes_handoff_*.py" in contract["hygiene"]["line_limit_globs"]
def test_hux_replacement_modules_are_quality_managed():
"""The deleted prototype helpers are replaced by the production foundation and hook modules."""
contract = load_contract()
expected = {
"dockerfiles/hermes-hux-foundation/hux/budgets.py",
"dockerfiles/hermes-hux-foundation/hux/contracts.py",
"dockerfiles/hermes-hux-foundation/hux/errors.py",
"dockerfiles/hermes-hux-foundation/hux/flags.py",
"dockerfiles/hermes-hux-foundation/hux/http.py",
"dockerfiles/hermes-hux-foundation/hux/identity.py",
"dockerfiles/hermes-hux-foundation/hux/policy.py",
"dockerfiles/hermes-hux-foundation/hux/redaction.py",
"dockerfiles/hermes-hux-foundation/hux/rules.py",
"dockerfiles/hermes-worker-hux/hux_hook/client.py",
"dockerfiles/hermes-worker-hux/hux_hook/hooks.py",
}
for paths in (
contract["managed_modules"],
contract["lint_paths"],
contract["coverage"]["tracked_files"],
contract["coverage"]["branch_tracked_files"],
):
assert expected <= set(paths)
assert "services/hermes/scripts/hux_contracts.py" not in paths
assert "services/hermes/scripts/hux_policy.py" not in paths
assert "dockerfiles/hermes-hux-foundation/hux/*.py" in contract["hygiene"]["line_limit_globs"]
assert "dockerfiles/hermes-worker-hux/hux_hook/*.py" in contract["hygiene"]["line_limit_globs"]
def test_docs_check_reports_missing_docstring_and_missing_path(tmp_path: Path):
module_path = tmp_path / "managed.py"
module_path.write_text("value = 1\n", encoding="utf-8")