"""Governance rules behind the HUX contracts. Pure functions only: state machines for memory, approvals and releases, the autonomy capability matrix, the friendly-mode catalog and its Switchyard mapping, the sensitive-topic policy, suggestion gating and the feature-flag registry. The chat router and the WebUI both consume these rules; keeping them here means one implementation is tested and the other only serialises it. """ from __future__ import annotations import os from datetime import datetime, timedelta, timezone from typing import Any from hux_contracts import load_flags EFFORT_ORDER = ("low", "medium", "high", "xhigh") MEMORY_TRANSITIONS: dict[str, frozenset[str]] = { "proposed": frozenset({"active", "rejected"}), "active": frozenset({"expired", "forgotten"}), "expired": frozenset({"forgotten"}), "rejected": frozenset(), "forgotten": frozenset(), } APPROVAL_TRANSITIONS: dict[str, frozenset[str]] = { "pending": frozenset({"approved", "denied", "expired", "cancelled"}), "approved": frozenset(), "denied": frozenset(), "expired": frozenset(), "cancelled": frozenset(), } RELEASE_ORDER = ("reviewed", "merged", "built", "verified", "deployed", "converged", "live_verified") RELEASE_EVIDENCE: dict[str, tuple[str, ...]] = { "merged": ("merge_commit",), "built": ("ci_build_url", "image_digest"), "verified": ("harbor_digest",), "deployed": ("flux_revision",), "converged": ("pod_digest",), "live_verified": ("health_check",), "rolled_back": ("rollback_target",), } CAPABILITIES = ( "read_files", "write_files", "shell", "network", "web_search", "send_message", "memory_write", "artifact_write", "spend_tokens", "delegate", "deploy", ) _READ_ONLY = frozenset({"read_files", "web_search", "spend_tokens"}) _MUTATING = frozenset({"write_files", "shell", "send_message", "memory_write", "artifact_write", "delegate"}) _EXTERNAL = frozenset({"network", "deploy"}) MODE_CATALOG: dict[str, dict[str, Any]] = { "fast": { "label": "Fast", "intent": "Quick answers and small edits. Lowest latency wins; depth is not expected.", "providers": ["codex", "claude"], "local_only": False, "effort": {"min": "low", "max": "medium"}, "tools": {"web": "allowed", "shell": "denied", "artifacts": "allowed", "delegate": "denied"}, "memory": {"read": True, "write": True}, "citations_required": False, "retention": "default", "route_id": "atlas/auto/fast", }, "thoughtful": { "label": "Thoughtful", "intent": "Careful reasoning on one problem. Takes longer, checks its own work, may run tools.", "providers": ["codex", "claude"], "local_only": False, "effort": {"min": "medium", "max": "xhigh"}, "tools": {"web": "allowed", "shell": "allowed", "artifacts": "allowed", "delegate": "allowed"}, "memory": {"read": True, "write": True}, "citations_required": False, "retention": "default", "route_id": "atlas/auto/deep", }, "research": { "label": "Research", "intent": "Find, read and cite sources. Every factual claim carries a citation with a support verdict; assumptions and open questions stay visible.", "providers": ["codex", "claude"], "local_only": False, "effort": {"min": "high", "max": "xhigh"}, "tools": {"web": "required", "shell": "denied", "artifacts": "allowed", "delegate": "allowed"}, "memory": {"read": True, "write": True}, "citations_required": True, "retention": "default", "route_id": "atlas/auto/deep", }, "create": { "label": "Create", "intent": "Produce and iterate on a durable artifact: documents, code, pages, images. Versions are kept and diffable.", "providers": ["codex", "claude"], "local_only": False, "effort": {"min": "medium", "max": "high"}, "tools": {"web": "allowed", "shell": "allowed", "artifacts": "encouraged", "delegate": "allowed"}, "memory": {"read": True, "write": True}, "citations_required": False, "retention": "default", "route_id": "atlas/auto/balanced", }, "private": { "label": "Private", "intent": "Nothing leaves the cluster. Local model only, no memory writes, no web, conversation not retained beyond the session.", "providers": ["local"], "local_only": True, "effort": {"min": "low", "max": "medium"}, "tools": {"web": "denied", "shell": "denied", "artifacts": "allowed", "delegate": "denied"}, "memory": {"read": False, "write": False}, "citations_required": False, "retention": "ephemeral", "route_id": "atlas/manual/local/qwen-14b", }, } PRIVACY_TOPICS: dict[str, dict[str, Any]] = { "health": {"sensitivity": "sensitive", "memory_write": "ask", "decay_days": 30}, "finance": {"sensitivity": "sensitive", "memory_write": "ask", "decay_days": 30}, "legal": {"sensitivity": "sensitive", "memory_write": "ask", "decay_days": 30}, "relationships": {"sensitivity": "sensitive", "memory_write": "ask", "decay_days": 14}, "credentials": {"sensitivity": "restricted", "memory_write": "deny", "decay_days": 1}, "minors": {"sensitivity": "restricted", "memory_write": "deny", "decay_days": 7}, "location": {"sensitivity": "restricted", "memory_write": "deny", "decay_days": 7}, "biometric": {"sensitivity": "restricted", "memory_write": "deny", "decay_days": 1}, } _NOTICE = "This looks like a {topic} topic. It stays in this conversation and is not remembered unless you say so." def transition_allowed(table: dict[str, frozenset[str]], current: str, target: str) -> bool: """True when ``current -> target`` is a legal move in ``table``.""" return target in table.get(current, frozenset()) def memory_policy_violations(entry: dict[str, Any]) -> list[str]: """Rules the schema cannot express: sensitivity drives approval, TTL and topic.""" problems: list[str] = [] sensitivity = entry.get("sensitivity") topic = entry.get("topic", "general") if sensitivity == "restricted" and entry.get("status") in {"proposed", "active"}: problems.append("restricted content may not be remembered") if sensitivity == "sensitive" and entry.get("approval_mode") != "ask": problems.append("sensitive memory requires approval_mode=ask") if sensitivity == "sensitive" and entry.get("ttl", {}).get("policy") == "never": problems.append("sensitive memory must expire or decay") if topic in PRIVACY_TOPICS and PRIVACY_TOPICS[topic]["memory_write"] == "deny" and entry.get("status") != "rejected": problems.append(f"topic {topic} may not be written to memory") ttl = entry.get("ttl", {}) if ttl.get("policy") == "expires_at" and "expires_at" not in ttl: problems.append("ttl.policy=expires_at requires expires_at") if ttl.get("policy") == "decay" and "decay_days" not in ttl: problems.append("ttl.policy=decay requires decay_days") if entry.get("status") == "forgotten" and entry.get("content"): problems.append("forgotten entries must drop their content") return problems def default_capability_matrix() -> dict[str, dict[str, str]]: """Autonomy level -> capability -> allow|ask|deny. Deploy always asks.""" matrix: dict[str, dict[str, str]] = {} for level in ("ask_first", "safe", "autonomous"): row: dict[str, str] = {} for capability in CAPABILITIES: if capability == "deploy": row[capability] = "ask" elif capability in _READ_ONLY: row[capability] = "allow" elif capability in _MUTATING: row[capability] = "ask" if level != "autonomous" else "allow" elif capability in _EXTERNAL: row[capability] = {"ask_first": "ask", "safe": "deny", "autonomous": "allow"}[level] matrix[level] = row return matrix def effective_decision(policy: dict[str, Any], capability: str, now: datetime | None = None) -> str: """Resolve one capability under a policy: explicit unexpired grant beats the matrix, deny beats all.""" now = now or datetime.now(timezone.utc) matrix = default_capability_matrix()[policy["autonomy"]] decision = matrix[capability] for grant in policy.get("grants", []): if grant["capability"] != capability: continue expires = grant.get("expires_at") if expires and datetime.fromisoformat(expires.replace("Z", "+00:00")) <= now: continue if grant["decision"] == "deny": return "deny" decision = grant["decision"] if capability == "deploy" and decision == "allow": return "ask" return decision def release_transition_problems(record: dict[str, Any], target: str) -> list[str]: """Why ``record`` may not move to ``target``. Empty means it may.""" current = record["state"] problems: list[str] = [] if target == "rolled_back": if current in {"reviewed", "merged"}: problems.append("nothing to roll back before an image exists") elif current == "rolled_back": problems.append("a rolled back release is terminal; open a new release record") elif RELEASE_ORDER.index(target) != RELEASE_ORDER.index(current) + 1: problems.append(f"{current} -> {target} skips or reverses the release order") evidence = record.get("evidence", {}) for key in RELEASE_EVIDENCE.get(target, ()): if key not in evidence: problems.append(f"{target} requires evidence.{key}") if target == "verified" and evidence.get("harbor_digest") != evidence.get("image_digest"): problems.append("harbor_digest must equal image_digest") if target == "converged" and evidence.get("pod_digest") != evidence.get("image_digest"): problems.append("pod_digest must equal image_digest") if target == "live_verified" and evidence.get("health_check", {}).get("status") != "pass": problems.append("health_check must pass") return problems def mode_contract(mode: str, override_route_id: str | None = None) -> dict[str, Any]: """Serialise one friendly mode as a ``hux.mode.v1`` record.""" spec = MODE_CATALOG[mode] switchyard: dict[str, Any] = {"route_id": spec["route_id"]} if override_route_id: if spec["local_only"] and "/local/" not in override_route_id: raise ValueError("private mode cannot be overridden to a hosted route") switchyard["override_route_id"] = override_route_id return { "schema": "hux.mode.v1", "mode": mode, "label": spec["label"], "intent": spec["intent"], "constraints": { key: spec[key] for key in ("providers", "local_only", "effort", "tools", "memory", "citations_required", "retention") }, "switchyard": switchyard, } def effort_within(mode: str, effort: str) -> bool: """True when ``effort`` sits inside the mode's allowed band.""" band = MODE_CATALOG[mode]["effort"] index = EFFORT_ORDER.index(effort) return EFFORT_ORDER.index(band["min"]) <= index <= EFFORT_ORDER.index(band["max"]) def privacy_policy(version: int = 1) -> dict[str, Any]: """Serialise the sensitive-topic policy as a ``hux.privacy_policy.v1`` record.""" return { "schema": "hux.privacy_policy.v1", "version": version, "topics": [ {"topic": topic, **rule, "notice": _NOTICE.format(topic=topic)} for topic, rule in PRIVACY_TOPICS.items() ], "topic_scoping": {"scope_to_conversation": True, "cross_surface_sharing": "never"}, "retention_audit": { "interval_days": 1, "actions": ["expire_memory", "decay_topic_context", "purge_forgotten_content", "report"], }, } def suggestion_allowed(suggestion: dict[str, Any], state: dict[str, Any] | None, now: datetime) -> bool: """Suppression state wins: never-again, dismissed, exhausted shows, or cooling down all block.""" if state is None: return True if state.get("never_again") or state.get("dismissed_at") or state.get("acted_at"): return False rules = suggestion["suppression"] if state.get("shows", 0) >= rules["max_shows"]: return False last = state.get("last_shown_at") if last: shown = datetime.fromisoformat(last.replace("Z", "+00:00")) if now - shown < timedelta(seconds=rules["cooldown_seconds"]): return False return True def flag_registry() -> dict[str, dict[str, Any]]: """Card id -> registry entry.""" return {card["card"]: card for card in load_flags()["cards"]} def enabled_flags(environ: dict[str, str] | None = None) -> set[str]: """Flags switched on through the HUX_FLAGS comma list; unknown names are ignored.""" environ = os.environ if environ is None else environ known = {card["flag"] for card in flag_registry().values()} raw = environ.get(load_flags()["env_var"], "") return {item.strip() for item in raw.split(",") if item.strip() in known} def flag_enabled(flag: str, environ: dict[str, str] | None = None) -> bool: """A flag counts only when it and every card it depends on are enabled.""" registry = flag_registry() by_flag = {card["flag"]: card for card in registry.values()} if flag not in by_flag: return False on = enabled_flags(environ) pending = [by_flag[flag]] while pending: card = pending.pop() if card["flag"] not in on: return False pending.extend(registry[dep] for dep in card["depends_on"]) return True def dependency_order() -> list[str]: """Cards in an order that satisfies depends_on; raises on cycles.""" registry = flag_registry() done: list[str] = [] visiting: set[str] = set() def visit(card_id: str) -> None: if card_id in done: return if card_id in visiting: raise ValueError(f"dependency cycle through {card_id}") visiting.add(card_id) for dep in registry[card_id]["depends_on"]: visit(dep) visiting.discard(card_id) done.append(card_id) for card_id in sorted(registry): visit(card_id) return done