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
314 lines
14 KiB
Python
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
|