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
239 lines
13 KiB
Markdown
239 lines
13 KiB
Markdown
# 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` (1–3 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 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):
|
||
|
||
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.
|