atlas-iac/docs/hux/WORKER-PLAN.md
jenkins c0a9c92ee4 hermes(worker): stage inert HUX foundation on the worker instance
worker.bstein.dev (the hermes-agent Deployment) gains the same HUX
shape as chat, staged and inert: a foundation-only hux sidecar on the
reviewed WebUI image line (Flux setters bound, 5s probe budgets), an
init that provisions the HMAC identity as slot-100 on the durable home
subtree (create-once context key, O_EXCL subject binding, per-pod
worker key; no relay/router/evidence keys so those trusts fail closed),
and observe-only hook env in the agent container with the runtime
plugin mounted but deliberately NOT enabled - activation is a reviewed
one-line flip per docs/hux/WORKER-PLAN.md, which carries the rollout,
verification gates, canary/rollback ladder and open questions.
Cross-surface continuity remains unclaimed until the live gates pass.
7 new topology-adaptive delivery gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 13:54:27 -03:00

13 KiB
Raw Blame History

Worker HUX integration plan (worker.bstein.dev)

Status: PREPARED, working tree only — reviewed and applied at a controlled moment by the coordinator. The hermes-agent pod is the live automation brain; nothing here is pushed, merged, or reconciled by the preparing agent.

Which workload is the Worker

worker.bstein.dev → Ingress hermes-agent-dashboard → Service oauth2-proxy-hermes-agent (selector app: hermes-agent) → the oauth2-proxy container inside the hermes-agent Deployment (services/hermes/agent-deployment.yaml), which proxies loopback 127.0.0.1:9119 (dashboard) and 127.0.0.1:7681 (terminal). The hermes Deployment in services/hermes/deployment.yaml is the DASHBOARD (triage) workload and is untouched. All edits land in agent-deployment.yaml + agent-configmap.yaml only.

Design summary

Mirrors the reviewed chat topology (chat-statefulset.yaml), adapted to a single-replica Recreate Deployment:

  • hux sidecar on the SAME reviewed WebUI image line as chat (registry.bstein.dev/bstein/hermes-webui, Flux marker hermes:hermes-webui-release, plus :tag/:digest setters for HUX_IMAGE_TAG/HUX_IMAGE_DIGEST). python -m hux.server, PYTHONPATH=/opt/hermes-hux, binds 127.0.0.1:8790 only; port 8790 appears in no Service, oauth2-proxy upstream, or NetworkPolicy. Probes are loopback /healthz exec probes with the corrected 5s timeout budget (the chat canary's 2s budget caused transient probe kills).
  • Flags: HUX_FLAGS=hux.foundation only. HUX_TOOL_ENFORCEMENT=0 (observe-only). Card widening and enforcement are separate, gated commits.
  • Identity: new init-hux-runtime init container provisions, exactly as chat's (same HMAC scheme usr_ + HMAC-SHA256(context-key, "hux.subject.id.v1\0<slot>")), a persistent 32-byte context key (0600), an immutable O_EXCL subject binding (0440, verified on every restart), a redaction canary, and a worker transport key in a Memory emptyDir (0400, regenerated every pod replacement). Slot is pinned to slot-100 (the service's SLOT_RE requires slot-[0-9]{1,3}; 100 is disjoint from the chat tenants' slot-0..3). Surface worker, trust worker. No relay key, no router key, no evidence key: with those env/file inputs absent, hux/identity.py fails those trusts closed — worker trust is the only live caller class on this instance.
  • Storage: the store is a subtree of the existing durable hermes-agent-home PVC — hux/ at the claim root, mounted into the sidecar as /var/lib/hux via fixed subPath: hux (NOT $(POD_NAME): Deployment pod names churn, and identity must survive replacement). The hermes container receives only read-only views: /run/hermes-hux-context = hux/context, /run/hermes-hux-subject = hux/binding. Chat's hermes-chat-hux-data claim is not referenced.
  • Agent hook wiring (hermes gateway container): HUX_BASE_URL, HUX_RUNTIME_ENABLED=1, HUX_TOOL_ENFORCEMENT=0, HUX_TENANT_SLOT=slot-100, HUX_WORKER_KEY_FILE, HUX_SUBJECT_FILE, HUX_CONTEXT_KEY_FILE, HUX_TIMEOUT_SECONDS=3, per dockerfiles/hermes-worker-hux/hux_hook/NOTES.md. No key or subject is ever a literal env value. The hermes-hux-runtime-plugin ConfigMap (already generated by kustomization.yaml for chat) is mounted read-only at /opt/data/plugins/hux-runtime, but hux-runtime is deliberately absent from plugins.enabled in agent-configmap.yaml, so the whole hook is inert until the activation commit flips that one line.

Storage justification (home-PVC subtree vs dedicated PVC)

Chosen: hermes-agent-home subtree. Reasons:

  1. Self-contained: no new PVC resource. pvc.yaml and kustomization.yaml stay untouched, and the pod can never be Pending on an unbound claim — this pod is the live automation brain and must always schedule.
  2. Single replica + Recreate strategy = single writer on the RWO claim; chat needed RWX + subPathExpr because four pods share one claim. Here one pod owns everything.
  3. Durability is identical to the rest of the coordinator state (Kanban DB, workspace) — one claim to back up, one failure domain.

Honest caveat, stated for review: the hermes container runs as root and mounts the whole home claim at /opt/data, so the store subtree is technically readable at /opt/data/hux by the agent process. On chat the dedicated claim makes the store physically unreachable; here the boundary is "the agent only uses the loopback API" plus 0700 modes (which do not bind root). This is acceptable for a single-operator, single-subject instance whose pod is already one trust domain — but if harder isolation is wanted, the follow-up in "Coordinator TODOs" moves the store to a dedicated hermes-agent-hux-data claim (a two-line volume swap plus data copy).

Release rendering / Flux automation impact (coordinator, do not edit ci/)

  • Flux ImageUpdateAutomation for hermes uses update: {strategy: Setters, path: services/hermes} — the new markers in agent-deployment.yaml are inside that path, so Flux keeps the sidecar image + tag/digest bindings current with zero renderer changes.
  • ci/scripts/hermes_webui_flux_release.py rewrites only StatefulSet/hermes-chat-tenant (13 consumers) and Deployment/hermes (exactly 1); it never opens agent-deployment.yaml, so its expected_images contracts are unaffected. test_hermes_image_automation.py counts markers only in the chat and dashboard files — verified unaffected.
  • OPTIONAL renderer extension (coordinator decision): add a third target (agent_manifest, "Deployment", "hermes-agent", "hermes-agent-deployment.yaml", 1) plus HUX build-metadata binding for the agent file, so the release belt covers the Worker too. Requires matching updates to test_hermes_webui_release.py expectations. Not required for correctness (Flux setters are authoritative); listed for completeness.
  • Operational consequence: once merged, every WebUI release digest bump rewrites agent-deployment.yaml and therefore rolls the hermes-agent pod (Recreate ⇒ brief worker.bstein.dev outage, minutes) on every WebUI release. Chat already rolls on those releases; the Worker joining that cadence must be an accepted trade. If not accepted, the alternative is pinning the sidecar image without markers and bumping it manually — that deviates from the "same reviewed image line" requirement and is NOT what is prepared here.

Coordinator TODOs (files outside this prepare's ownership)

  1. None required for kustomize/dry-run correctness. All referenced objects (hermes-hux-runtime-plugin ConfigMap, hermes-agent-home PVC) already exist in the build. testing/quality_contract.json already globs testing/**/*.py, so the new test needs no registration.
  2. At activation (separate commit, after staged-state gates pass):
    • add - hux-runtime to plugins.enabled in agent-configmap.yaml;
    • update testing/tests/test_hermes_agent_layout.py (test_agent_uses_one_native_kanban_control_plane asserts config["plugins"]["enabled"] == ["auto-router"] — exact equality) to the new list. The prepared delivery test intentionally pins the staged state ("hux-runtime" not in plugins.enabled) and must be updated in the same activation commit.
  3. Optional hardening: dedicated hermes-agent-hux-data PVC (RWO, astreae, 5Gi) in pvc.yaml + swap the sidecar's home/subPath: hux volume for it; copy hux/ content across during a quiet window.
  4. Optional: renderer third target (above).
  5. Pre-existing note: test_hermes_ai_usage_exporter.py pins ai.bstein.dev/config-rev == "20260823-dual-provider-quota-health" while the manifest carries "20260823-image-release-automation" — mismatched before this work; the config-rev annotation was deliberately left untouched here.

Rollout steps (exact)

  1. Review this diff; run the full gate locally (PYTHONPATH=. .venv/bin/pytest -q testing/tests/test_hermes_worker_hux_delivery.py plus the agent/webui suites) and kustomize build services/hermes.
  2. Merge to main in a quiet window (no cli-lane tasks in flight: check the Kanban board and /api/status; the Recreate roll kills live worker sessions — durable CLI lanes resume, native children do not).
  3. Let Flux reconcile. The pod recreates once with the sidecar. Staged state: hux serving loopback foundation-only; hook completely inert (plugin not enabled).
  4. Verify the staged gates (below). Leave the staged state soaking for at least one WebUI release cycle to observe the digest-roll behavior.
  5. Activation commit (coordinator TODO 2) — the hook starts observing: events/spend posted from real worker turns, zero enforcement.
  6. Card widening (activity_timeline first), each with its lifecycle gate and a delivery-test flag-pin update. hux.autonomy is BLOCKED on the human-surface question below. Enforcement stays 0 throughout this plan.

Verification gates (all must pass before any wider claim)

Staged (after step 3):

  • Pod hermes-agent-* all containers Ready, zero restart loops; public https://worker.bstein.dev 200 behind SSO; terminal reachable.
  • Sidecar loopback health: from the hux container, python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8790/healthz').read())"status: ok.
  • Identity provisioning: hux/binding/subject exists (0440, usr_ + 64 hex), hux/context/context-key 32 bytes 0600; record sha256(context-key) and the subject value.
  • Capabilities matrix on the loopback: no headers → 401; worker key + wrong slot → 401; worker key + slot-100 + bound subject → 200 with ONLY HUX-11 foundation routes; any card route → 404 flag_off. Confirm relay trust 401s (no relay key exists).
  • Identity persistence across a pod replacement: delete the pod (or ride the next WebUI release roll); after the new pod is Ready, re-hash context-key and re-read the subject — both MUST be identical. The worker-key MUST differ (rotation proof).
  • Store isolation spot check: no Service/NetworkPolicy exposes 8790; store dirs 0700 uid 10000.

Activated (after step 5):

  • Drive one real worker turn (dashboard chat or a Kanban dispatch) and read GET /hux/v1/conversations/…/events via worker trust on the loopback: the turn's events appear with Idempotency-Key-deduped rows and no raw arguments/output in any summary (redaction canary from hux/context/redaction-canary never appears in stored records).
  • Gateway, terminal, cli-lane-runner all still Ready; a cli-lane task completes end-to-end (the hook must never block at enforcement 0).

Cross-surface continuity is NOT claimed until every gate above has passed on the live Worker — and even then it is a separate, explicit follow-up (the chat and worker instances have distinct subjects and stores by design; any continuity feature is new work, not a side effect of this rollout).

Canary / rollback

This is a single-replica Deployment: there is no partition canary. The canary IS the staged inert state (steps 34) — sidecar live, hook off — held for a soak period before activation. Roll it as a Flux-tracked change only.

Rollback ladder (fastest first):

  1. Activation rollback: revert the activation commit (remove hux-runtime from plugins.enabled). One pod roll; sidecar keeps running; hook inert again.
  2. Full rollback: revert the integration commit(s) touching agent-deployment.yaml/agent-configmap.yaml. Flux recreates the pod without sidecar/init/env. The hux/ subtree on the home claim is retained data, not referenced by anything — harmless, removable later.
  3. Emergency (cluster is broken, Flux unusable): Brad applies kubectl -n hermes rollout undo deployment/hermes-agent — then reconciles Git to match, since the agent itself must never mutate workloads.

What to watch during any roll: kubectl -n hermes get pods -w until ALL containers Ready (13 app containers once rendered: the 12 in agent-deployment.yaml including hux, plus execution-pool-coordinator from execution-coordinator-patch.yaml); https://worker.bstein.dev/api/status 200; Kanban dispatch resumes (cli-lane-runner readiness); Vault agent-inject succeeded (init order unchanged); zero CrashLoopBackOff on hux.

Open questions

  1. Approvals need a human surface. This instance has only worker trust; hux.autonomy decisions are rejected for worker callers by design (F1), so enabling autonomy/enforcement on the Worker requires a human-surface path first (candidates: a BFF on the worker dashboard asserting router trust like chat's, or driving decisions from the chat instance — which reopens cross-surface identity). Until decided, hux.autonomy and HUX_TOOL_ENFORCEMENT=1 are out of scope.
  2. Terminal and cli-lane-runner containers run their own Hermes processes but were intentionally NOT wired (env absent → plugin HUX_RUNTIME_ENABLED gate keeps them clean even after activation). If observation should cover TUI/lane turns too, mirror the hermes container's env+mounts onto them in a follow-up.
  3. HUX_PROJECT_SOURCE is profile:default for parity with chat; a worker-specific source (e.g. profile:worker) would partition worker activity into its own HUX project — decide before activation, changing it later re-keys the derived project id.
  4. Whether to take Coordinator TODO 3 (dedicated PVC) before or after activation.