jenkins 6964a9d8c8 hermes(hux): close Wave A review findings in autonomy and the HTTP pipeline
F1 policy writes and allow grants are human-surface only; F2 worker trust is
confined to the hook allowlist and unexpected exceptions become audited 500
error records; F4 external side effects release only for the same run and
argument hash; F6 the gate honours budget exhaustion; F8 only the gateway
can vouch for an empty process registry and failed receipts can be
superseded; F11/F12 receipt revision and unshipped card routes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNPhwu2bsaRNg3DETSAZoM
2026-08-24 00:48:20 -03:00

98 lines
5.0 KiB
Python

"""Per-card feature flags and the capabilities record clients negotiate with.
Flags come from the ``HUX_FLAGS`` comma list. A card counts as enabled only
when it and every card it depends on are enabled, so a half-configured
deployment fails closed. Route ownership per card is declared here so the
capabilities record can tell a client exactly what it may call, and the
worker allowlist (SO-08) says which of those a ``trust: worker`` caller may
reach at all.
"""
from __future__ import annotations
import os
from collections.abc import Mapping
from hux.errors import FlagOff
from hux.identity import Identity
from hux.rules import flag_enabled, flag_registry
CONTRACT_VERSION = "1.1.0"
CARD_ROUTES: dict[str, list[str]] = {
"HUX-11": ["/hux/v1/capabilities", "/hux/v1/manifest"],
"HUX-01": ["/hux/v1/conversations/{id}/events", "/hux/v1/conversations/{id}/events/stream"],
"HUX-02": ["/hux/v1/memory", "/hux/v1/memory/{id}", "/hux/v1/memory/{id}/{action}", "/hux/v1/memory/export"],
"HUX-03": ["/hux/v1/projects", "/hux/v1/projects/{id}", "/hux/v1/conversations", "/hux/v1/conversations/{id}", "/hux/v1/conversations/{id}/branch", "/hux/v1/conversations/{id}/lineage", "/hux/v1/search"],
"HUX-04": ["/hux/v1/artifacts", "/hux/v1/artifacts/{id}", "/hux/v1/artifacts/{id}/versions", "/hux/v1/artifacts/{id}/versions/{n}", "/hux/v1/artifacts/{id}/versions/{n}/diff", "/hux/v1/artifacts/{id}/promote"],
"HUX-05": ["/hux/v1/policy", "/hux/v1/approvals", "/hux/v1/approvals/{id}", "/hux/v1/runs/{id}/stop", "/hux/v1/runs/{id}/budget", "/hux/v1/runs/{id}/gate"],
"HUX-06": [],
"HUX-07": [],
"HUX-08": ["/hux/v1/sources", "/hux/v1/sources/{id}", "/hux/v1/passages", "/hux/v1/messages/{id}/citations", "/hux/v1/notebooks", "/hux/v1/notebooks/{id}"],
"HUX-09": [],
"HUX-10": ["/hux/v1/privacy/policy", "/hux/v1/privacy/notices", "/hux/v1/conversations/{id}/forget", "/hux/v1/privacy/audit"],
"HUX-12": [],
}
# Cards whose routes are not shipped yet declare [] above until they land (F12).
# The only (method, template) pairs a ``trust: worker`` caller may reach (SO-08).
# Everything else is 403 before the flag check. These are the agent-hook
# routes: negotiation, the approval/gate/budget/stop loop, activity events,
# the privacy policy, memory retrieval and proposals, research inputs and
# 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"),
("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"),
("POST", "/hux/v1/conversations/{id}/events"), ("GET", "/hux/v1/conversations/{id}"),
("GET", "/hux/v1/privacy/policy"),
("GET", "/hux/v1/memory"), ("POST", "/hux/v1/memory"),
("POST", "/hux/v1/sources"), ("POST", "/hux/v1/passages"), ("POST", "/hux/v1/messages/{id}/citations"),
("POST", "/hux/v1/artifacts"), ("POST", "/hux/v1/artifacts/{id}/versions"),
})
def worker_may_call(method: str, template: str) -> bool:
"""True when a ``trust: worker`` caller is allowed on this route (SO-08)."""
return (method, template) in WORKER_ROUTES
class Flags:
"""Snapshot of which cards are on for this process."""
def __init__(self, environ: Mapping[str, str] | None = None) -> None:
self._environ = dict(os.environ if environ is None else environ)
self._registry = flag_registry()
def enabled(self, card: str) -> bool:
"""True when the card and its whole dependency chain are on."""
entry = self._registry.get(card)
return bool(entry) and flag_enabled(entry["flag"], self._environ)
def require(self, card: str) -> None:
"""Raise FlagOff unless the card is enabled."""
if not self.enabled(card):
raise FlagOff(f"{card} is not enabled")
def capabilities(self, identity: Identity, build: Mapping[str, str] | None = None) -> dict:
"""Serialise ``hux.capabilities.v1`` for one caller."""
cards = [
{"card": card, "flag": entry["flag"], "enabled": self.enabled(card), "routes": CARD_ROUTES.get(card, [])}
for card, entry in sorted(self._registry.items())
]
server = {k: v for k, v in (build or {}).items() if k in {"commit", "image_digest"} and v}
return {
"schema": "hux.capabilities.v1",
"contract_version": CONTRACT_VERSION,
"identity": identity.record(),
"cards": cards,
"server": server,
}
def build_from_environ(environ: Mapping[str, str] | None = None) -> dict[str, str]:
"""Commit and image digest the pod was started with, when the operator set them."""
environ = os.environ if environ is None else environ
return {"commit": environ.get("HUX_BUILD_COMMIT", ""), "image_digest": environ.get("HUX_IMAGE_DIGEST", "")}