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
13 KiB
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:
huxsidecar on the SAME reviewed WebUI image line as chat (registry.bstein.dev/bstein/hermes-webui, Flux markerhermes:hermes-webui-release, plus:tag/:digestsetters forHUX_IMAGE_TAG/HUX_IMAGE_DIGEST).python -m hux.server,PYTHONPATH=/opt/hermes-hux, binds127.0.0.1:8790only; port 8790 appears in no Service, oauth2-proxy upstream, or NetworkPolicy. Probes are loopback/healthzexec probes with the corrected 5s timeout budget (the chat canary's 2s budget caused transient probe kills).- Flags:
HUX_FLAGS=hux.foundationonly.HUX_TOOL_ENFORCEMENT=0(observe-only). Card widening and enforcement are separate, gated commits. - Identity: new
init-hux-runtimeinit container provisions, exactly as chat's (same HMAC schemeusr_ + HMAC-SHA256(context-key, "hux.subject.id.v1\0<slot>")), a persistent 32-byte context key (0600), an immutableO_EXCLsubject binding (0440, verified on every restart), a redaction canary, and a worker transport key in aMemoryemptyDir (0400, regenerated every pod replacement). Slot is pinned toslot-100(the service'sSLOT_RErequiresslot-[0-9]{1,3}; 100 is disjoint from the chat tenants' slot-0..3). Surfaceworker, trustworker. No relay key, no router key, no evidence key: with those env/file inputs absent,hux/identity.pyfails 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-homePVC —hux/at the claim root, mounted into the sidecar as/var/lib/huxvia fixedsubPath: hux(NOT$(POD_NAME): Deployment pod names churn, and identity must survive replacement). Thehermescontainer receives only read-only views:/run/hermes-hux-context=hux/context,/run/hermes-hux-subject=hux/binding. Chat'shermes-chat-hux-dataclaim is not referenced. - Agent hook wiring (
hermesgateway 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, perdockerfiles/hermes-worker-hux/hux_hook/NOTES.md. No key or subject is ever a literal env value. Thehermes-hux-runtime-pluginConfigMap (already generated bykustomization.yamlfor chat) is mounted read-only at/opt/data/plugins/hux-runtime, buthux-runtimeis deliberately absent fromplugins.enabledinagent-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:
- Self-contained: no new PVC resource.
pvc.yamlandkustomization.yamlstay untouched, and the pod can never be Pending on an unbound claim — this pod is the live automation brain and must always schedule. - Single replica +
Recreatestrategy = single writer on the RWO claim; chat needed RWX +subPathExprbecause four pods share one claim. Here one pod owns everything. - 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
ImageUpdateAutomationfor hermes usesupdate: {strategy: Setters, path: services/hermes}— the new markers inagent-deployment.yamlare inside that path, so Flux keeps the sidecar image + tag/digest bindings current with zero renderer changes. ci/scripts/hermes_webui_flux_release.pyrewrites onlyStatefulSet/hermes-chat-tenant(1–3 consumers) andDeployment/hermes(exactly 1); it never opensagent-deployment.yaml, so itsexpected_imagescontracts are unaffected.test_hermes_image_automation.pycounts 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 totest_hermes_webui_release.pyexpectations. Not required for correctness (Flux setters are authoritative); listed for completeness. - Operational consequence: once merged, every WebUI release digest bump
rewrites
agent-deployment.yamland 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)
- None required for kustomize/dry-run correctness. All referenced
objects (
hermes-hux-runtime-pluginConfigMap,hermes-agent-homePVC) already exist in the build.testing/quality_contract.jsonalready globstesting/**/*.py, so the new test needs no registration. - At activation (separate commit, after staged-state gates pass):
- add
- hux-runtimetoplugins.enabledinagent-configmap.yaml; - update
testing/tests/test_hermes_agent_layout.py(test_agent_uses_one_native_kanban_control_planeassertsconfig["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.
- add
- Optional hardening: dedicated
hermes-agent-hux-dataPVC (RWO, astreae, 5Gi) inpvc.yaml+ swap the sidecar'shome/subPath: huxvolume for it; copyhux/content across during a quiet window. - Optional: renderer third target (above).
- Pre-existing note:
test_hermes_ai_usage_exporter.pypinsai.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)
- Review this diff; run the full gate locally
(
PYTHONPATH=. .venv/bin/pytest -q testing/tests/test_hermes_worker_hux_delivery.pyplus the agent/webui suites) andkustomize build services/hermes. - Merge to
mainin 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). - Let Flux reconcile. The pod recreates once with the sidecar. Staged state: hux serving loopback foundation-only; hook completely inert (plugin not enabled).
- Verify the staged gates (below). Leave the staged state soaking for at least one WebUI release cycle to observe the digest-roll behavior.
- Activation commit (coordinator TODO 2) — the hook starts observing: events/spend posted from real worker turns, zero enforcement.
- Card widening (activity_timeline first), each with its lifecycle gate and
a delivery-test flag-pin update.
hux.autonomyis 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; publichttps://worker.bstein.dev200 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/subjectexists (0440,usr_+ 64 hex),hux/context/context-key32 bytes 0600; recordsha256(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 → 404flag_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/…/eventsvia worker trust on the loopback: the turn's events appear withIdempotency-Key-deduped rows and no raw arguments/output in any summary (redaction canary fromhux/context/redaction-canarynever 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 3–4) — sidecar live, hook off — held for a soak period before activation. Roll it as a Flux-tracked change only.
Rollback ladder (fastest first):
- Activation rollback: revert the activation commit (remove
hux-runtimefromplugins.enabled). One pod roll; sidecar keeps running; hook inert again. - Full rollback: revert the integration commit(s) touching
agent-deployment.yaml/agent-configmap.yaml. Flux recreates the pod without sidecar/init/env. Thehux/subtree on the home claim is retained data, not referenced by anything — harmless, removable later. - 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
- Approvals need a human surface. This instance has only worker trust;
hux.autonomydecisions 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.autonomyandHUX_TOOL_ENFORCEMENT=1are out of scope. - Terminal and cli-lane-runner containers run their own Hermes
processes but were intentionally NOT wired (env absent → plugin
HUX_RUNTIME_ENABLEDgate keeps them clean even after activation). If observation should cover TUI/lane turns too, mirror thehermescontainer's env+mounts onto them in a follow-up. HUX_PROJECT_SOURCEisprofile:defaultfor 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.- Whether to take Coordinator TODO 3 (dedicated PVC) before or after activation.