jenkins d3cb6e9045 hermes(hux): add contract-only foundation for the chat UX program
Schemas, examples and a flag registry for the twelve HUX cards, a dependency-free
validator, the governance rules (memory ledger, autonomy matrix, friendly modes
mapped to real Switchyard routes, privacy defaults, suggestion gating, release
state machine) and the contract doc UI work codes against.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNPhwu2bsaRNg3DETSAZoM
2026-08-23 22:30:41 -03:00

314 lines
14 KiB
Python

"""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