atlas-iac/services/hermes/agent-deployment.yaml
jenkins e54d581ef5 fix(hermes): un-pin worker from titan-08; spread across arm64 storage workers
Worker (hermes-agent) was hard-pinned to titan-08 (a workaround after an earlier
attempt to place it on the amd64 titan-22 failed on architecture). That single-
node pin is exactly what makes it fragile: a titan-08 blip (as just happened when
the node's Longhorn CSI went down) strands worker, and the Recreate strategy then
deadlocks because the replacement can't schedule on the one tight node.

Restore the intended multi-node design: run on any arm64 storage worker except the
known-bad/weak ones (matching the repo's own affinity test, which was red). Its
Longhorn volumes have data-locality disabled with replicas on titan-15/17/19, so
there is no locality penalty to running on another node; the scheduler now places
it on a roomier Pi (e.g. titan-05) and a node blip simply reschedules it.

Also relax the gateway /api/status liveness probe (timeout 10s->15s,
failureThreshold 3->5) so a transient slowness (e.g. a brief storage hiccup) no
longer trips a kill-and-restart cascade.

Follow-up (separate): multi-arch agent image to enable the amd64 titan-22 target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-25 09:30:06 -03:00

1526 lines
74 KiB
YAML

# services/hermes/agent-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hermes-agent
namespace: hermes
labels:
app: hermes-agent
spec:
replicas: 1
revisionHistoryLimit: 2
progressDeadlineSeconds: 2700
strategy:
type: Recreate
selector:
matchLabels:
app: hermes-agent
template:
metadata:
labels:
app: hermes-agent
annotations:
ai.bstein.dev/role: project-coordinator
ai.bstein.dev/router-wire-contract: ollama-numeric-keepalive
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
ai.bstein.dev/placement: titan-08 rpi5; storage-backbone nodes excluded
ai.bstein.dev/config-rev: "20260824-claude-fable-quota"
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "9010"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-agent
vault.hashicorp.com/agent-inject-containers: stage-runtime-access
vault.hashicorp.com/agent-inject-secret-agent-api-key: kv/data/atlas/hermes/agent-tokens
vault.hashicorp.com/agent-inject-template-agent-api-key: |
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
{{ .Data.data.agent_api_key }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-claude-credentials: kv/data/atlas/hermes/agent-tokens
vault.hashicorp.com/agent-inject-template-claude-credentials: |
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
{{ .Data.data.claude_credentials_json }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-claude-oauth-token: kv/data/atlas/hermes/agent-tokens
vault.hashicorp.com/agent-inject-template-claude-oauth-token: |
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
{{ .Data.data.claude_oauth_token }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-codex-auth: kv/data/atlas/hermes/agent-tokens
vault.hashicorp.com/agent-inject-template-codex-auth: |
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
{{ .Data.data.codex_auth_json }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-jenkins-image-build-token: kv/data/atlas/hermes/developer-jenkins
vault.hashicorp.com/agent-inject-template-jenkins-image-build-token: |
{{- with secret "kv/data/atlas/hermes/developer-jenkins" -}}
{{ .Data.data.build_token }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-node-ssh-private-key: kv/data/atlas/hermes/developer-ssh
vault.hashicorp.com/agent-inject-template-node-ssh-private-key: |
{{- with secret "kv/data/atlas/hermes/developer-ssh" -}}
{{ .Data.data.private_key }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-node-ssh-config: kv/data/atlas/hermes/developer-ssh
vault.hashicorp.com/agent-inject-template-node-ssh-config: |
{{- with secret "kv/data/atlas/hermes/developer-ssh" -}}
{{ .Data.data.config }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-node-ssh-known-hosts: kv/data/atlas/hermes/developer-ssh
vault.hashicorp.com/agent-inject-template-node-ssh-known-hosts: |
{{- with secret "kv/data/atlas/hermes/developer-ssh" -}}
{{ .Data.data.known_hosts }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-chat-relay-key: kv/data/atlas/hermes/chat-telegram
vault.hashicorp.com/agent-inject-template-chat-relay-key: |
{{- with secret "kv/data/atlas/hermes/chat-telegram" -}}
{{ .Data.data.relay_key }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-oidc-config: kv/data/atlas/hermes/agent-oidc
vault.hashicorp.com/agent-inject-template-oidc-config: |
{{- with secret "kv/data/atlas/hermes/agent-oidc" -}}
client_id = "{{ .Data.data.client_id }}"
client_secret = "{{ .Data.data.client_secret }}"
cookie_secret = "{{ .Data.data.cookie_secret }}"
{{- end -}}
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-requests-cpu: 25m
vault.hashicorp.com/agent-requests-mem: 32Mi
vault.hashicorp.com/agent-limits-cpu: 100m
vault.hashicorp.com/agent-limits-mem: 128Mi
spec:
serviceAccountName: hermes-agent
enableServiceLinks: false
automountServiceAccountToken: true
securityContext:
seccompProfile:
type: RuntimeDefault
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: [arm64]
- key: node-role.kubernetes.io/worker
operator: In
values: ["true"]
- key: kubernetes.io/hostname
operator: NotIn
values: [titan-04, titan-06, titan-08, titan-13, titan-14, titan-17, titan-18, titan-19]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: hardware
operator: In
values: [rpi5]
initContainers:
- name: init-config
image: busybox:1.37
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -eu
env_file=/opt/data/.env
profile_file=/opt/data/home/.profile
mkdir -p \
/opt/data/home/.claude \
/opt/data/home/.codex \
/opt/data/home/.kube \
/opt/data/home/.ssh \
/opt/data/cli-lanes \
/opt/data/logs \
/opt/data/provider-health \
/opt/data/tools/bin \
/opt/data/workspace/coordinator \
/opt/data/workspace/projects \
/opt/data/workspace/skills
if [ ! -e /opt/data/home/.hermes ]; then
ln -s /opt/data /opt/data/home/.hermes
fi
# OpenSSH resolves ~/.ssh from the account's passwd home
# (/opt/data), not the HOME override used by Hermes. Keep one
# Vault-populated directory visible through both paths.
if [ ! -e /opt/data/.ssh ] && [ ! -L /opt/data/.ssh ]; then
ln -s home/.ssh /opt/data/.ssh
fi
cp /config/config.yaml /opt/data/config.yaml
cp /config/SOUL.md /opt/data/SOUL.md
cp /config/AGENTS.md /opt/data/workspace/AGENTS.md
cp /config/START-HERE.md /opt/data/workspace/START-HERE.md
rm -f \
/opt/data/auth.json \
/opt/data/home/.claude/.credentials.json \
/opt/data/home/.codex/auth.json \
/opt/data/home/.ssh/config \
/opt/data/home/.ssh/known_hosts \
/opt/data/home/.ssh/id_ed25519_atlas_nodes
ln -s /runtime-access/node-ssh-config /opt/data/home/.ssh/config
ln -s /runtime-access/node-ssh-known-hosts /opt/data/home/.ssh/known_hosts
ln -s /runtime-access/node-ssh-private-key /opt/data/home/.ssh/id_ed25519_atlas_nodes
chmod 0700 /opt/data/home/.ssh
touch "${env_file}"
upsert_env() {
key="$1"
value="$2"
{ grep -v "^${key}=" "${env_file}" || true; printf '%s=%s\n' "${key}" "${value}"; } > "${env_file}.tmp"
mv "${env_file}.tmp" "${env_file}"
}
for key in ANTHROPIC_API_KEY API_SERVER_KEY CLAUDE_API_KEY CLAUDE_CODE_OAUTH_TOKEN GITEA_TOKEN GITEA_USERNAME HERMES_IMAGE_BROKER_KEY OPENAI_API_KEY; do
grep -v "^${key}=" "${env_file}" > "${env_file}.tmp" || true
mv "${env_file}.tmp" "${env_file}"
done
for profile_env in /opt/data/profiles/*/.env; do
[ -f "${profile_env}" ] || continue
for key in ANTHROPIC_API_KEY API_SERVER_KEY CLAUDE_API_KEY CLAUDE_CODE_OAUTH_TOKEN GITEA_TOKEN GITEA_USERNAME HERMES_IMAGE_BROKER_KEY OPENAI_API_KEY; do
grep -v "^${key}=" "${profile_env}" > "${profile_env}.tmp" || true
mv "${profile_env}.tmp" "${profile_env}"
done
chmod 0600 "${profile_env}"
chown 10000:10000 "${profile_env}"
done
upsert_env GIT_TERMINAL_PROMPT 0
upsert_env HERMES_SCM_BROKER_URL http://hermes-scm-broker.hermes-scm.svc.cluster.local:9081
upsert_env JENKINS_BASE_URL http://jenkins.jenkins.svc.cluster.local:8080
upsert_env ARIADNE_BASE_URL http://ariadne.maintenance.svc.cluster.local
upsert_env VICTORIA_METRICS_URL http://victoria-metrics-single-server.monitoring.svc.cluster.local:8428
upsert_env GRAFANA_BASE_URL https://metrics.bstein.dev
chmod 0600 "${env_file}"
touch "${profile_file}"
if ! grep -qxF '# Hermes managed operator PATH.' "${profile_file}"; then
printf '%s\n' \
'' \
'# Hermes managed operator PATH.' \
'case ":${PATH}:" in' \
' *":/opt/data/tools/bin:"*) ;;' \
' *) PATH="/opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/opt/hermes/bin:${PATH}" ;;' \
'esac' \
'export PATH' >> "${profile_file}"
fi
chmod 0644 "${profile_file}"
# Existing owner data is already written as uid/gid 10000. A
# recursive chown made every routine rollout walk the full 20Gi
# workspace while the dashboard had no endpoint. Own only the
# paths this init container creates or updates.
chown 10000:10000 \
/opt/data \
/opt/data/home \
/opt/data/home/.claude \
/opt/data/home/.codex \
/opt/data/home/.kube \
/opt/data/home/.ssh \
/opt/data/cli-lanes \
/opt/data/logs \
/opt/data/provider-health \
/opt/data/tools \
/opt/data/tools/bin \
/opt/data/workspace \
/opt/data/workspace/coordinator \
/opt/data/workspace/projects \
/opt/data/workspace/skills \
/opt/data/config.yaml \
/opt/data/SOUL.md \
/opt/data/workspace/AGENTS.md \
/opt/data/workspace/START-HERE.md \
"${profile_file}" \
"${env_file}"
securityContext:
allowPrivilegeEscalation: false
runAsUser: 0
runAsGroup: 0
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: home
mountPath: /opt/data
- name: config
mountPath: /config
readOnly: true
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 100m, memory: 64Mi}
# Worker HUX identity provisioning. Mirrors the chat tenants'
# init-hux-runtime, adapted to the single-replica Deployment: the
# subtree name is the fixed literal "hux" (Deployment pod names churn,
# so ${HOSTNAME} would break identity persistence across restarts) on
# the durable hermes-agent-home claim, and the slot is pinned to
# slot-100 — outside the chat tenants' slot-0..slot-3 range. Only a
# worker transport key is provisioned: the Worker has no relay or
# human browser surface, so relay trust stays fail-closed by absence.
- name: init-hux-runtime
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
umask 077
worker_root="/hux-data/hux"
export HUX_INIT_ROOT="${worker_root}"
mkdir -p \
"${worker_root}/binding" \
"${worker_root}/context" \
"${worker_root}/store" \
/hux-worker
chown 10000:10000 \
"${worker_root}" \
"${worker_root}/binding" \
"${worker_root}/context" \
"${worker_root}/store" \
/hux-worker
chmod 0700 \
"${worker_root}" \
"${worker_root}/binding" \
"${worker_root}/context" \
"${worker_root}/store" \
/hux-worker
if [ ! -e "${worker_root}/context/context-key" ]; then
dd if=/dev/urandom of="${worker_root}/context/.context-key.tmp" bs=32 count=1 2>/dev/null
chown 10000:10000 "${worker_root}/context/.context-key.tmp"
chmod 0600 "${worker_root}/context/.context-key.tmp"
mv "${worker_root}/context/.context-key.tmp" "${worker_root}/context/context-key"
fi
test "$(wc -c < "${worker_root}/context/context-key")" -eq 32
chown 10000:10000 "${worker_root}/context/context-key"
chmod 0600 "${worker_root}/context/context-key"
HUX_INIT_SLOT="slot-100" \
/opt/hermes/.venv/bin/python - <<'PY'
import hashlib
import hmac
import os
import stat
from pathlib import Path
root = Path(os.environ["HUX_INIT_ROOT"])
key = (root / "context/context-key").read_bytes()
slot = os.environ["HUX_INIT_SLOT"]
subject = "usr_" + hmac.new(
key,
b"hux.subject.id.v1\0" + slot.encode("ascii"),
hashlib.sha256,
).hexdigest()
target = root / "binding/subject"
expected = (subject + "\n").encode("ascii")
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(target, flags, 0o440)
except FileExistsError:
info = target.lstat()
if (
not stat.S_ISREG(info.st_mode)
or info.st_uid != 10000
or stat.S_IMODE(info.st_mode) != 0o440
or info.st_nlink != 1
or target.read_bytes() != expected
):
raise SystemExit("persistent HUX subject binding is unsafe")
else:
try:
os.write(descriptor, expected)
os.fchown(descriptor, 10000, 10000)
os.fchmod(descriptor, 0o440)
os.fsync(descriptor)
finally:
os.close(descriptor)
PY
if [ ! -e "${worker_root}/context/redaction-canary" ]; then
dd if=/dev/urandom bs=32 count=1 2>/dev/null \
| sha256sum | cut -d ' ' -f 1 \
> "${worker_root}/context/.redaction-canary.tmp"
chown 10000:10000 "${worker_root}/context/.redaction-canary.tmp"
chmod 0400 "${worker_root}/context/.redaction-canary.tmp"
mv "${worker_root}/context/.redaction-canary.tmp" "${worker_root}/context/redaction-canary"
fi
target=/hux-worker/worker-key
if [ ! -e "${target}" ]; then
dd if=/dev/urandom bs=32 count=1 2>/dev/null \
| sha256sum | cut -d ' ' -f 1 > "${target}.tmp"
chown 10000:10000 "${target}.tmp"
chmod 0400 "${target}.tmp"
mv "${target}.tmp" "${target}"
fi
test "$(wc -c < "${target}")" -eq 65
chown 10000:10000 "${target}"
chmod 0400 "${target}"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
add: [CHOWN, DAC_OVERRIDE, FOWNER]
runAsUser: 0
runAsGroup: 0
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /hux-data}
- {name: hux-worker-key, mountPath: /hux-worker}
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 50m, memory: 32Mi}
- name: stage-runtime-access
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/stage_runtime_access.py
- agent
securityContext:
allowPrivilegeEscalation: false
runAsUser: 0
runAsGroup: 0
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: claude-oauth-access, mountPath: /claude-oauth-access}
- {name: home, mountPath: /opt/data}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: oauth2-config, mountPath: /oauth2-config}
- {name: runtime-access, mountPath: /runtime-access}
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 100m, memory: 64Mi}
- name: install-agent-tools
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -eu
tools=/opt/data/tools
mkdir -p "${tools}/bin"
ttyd_version="$("${tools}/bin/ttyd" --version 2>/dev/null || true)"
case "${ttyd_version}" in *1.7.7*) ttyd_ready=1 ;; *) ttyd_ready=0 ;; esac
if [ "${ttyd_ready}" != "1" ]; then
curl -fsSL -o "${tools}/bin/ttyd.tmp" https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64
printf '%s %s\n' b38acadd89d1d396a0f5649aa52c539edbad07f4bc7348b27b4f4b7219dd4165 "${tools}/bin/ttyd.tmp" | sha256sum -c -
chmod 0755 "${tools}/bin/ttyd.tmp"
mv "${tools}/bin/ttyd.tmp" "${tools}/bin/ttyd"
fi
if [ ! -f "${tools}/.cli-versions-0.147.0-2.1.226" ]; then
npm install --global --omit=dev --no-audit --no-fund --prefix "${tools}" \
@openai/codex@0.147.0 \
@anthropic-ai/claude-code@2.1.226
touch "${tools}/.cli-versions-0.147.0-2.1.226"
fi
kubectl_version="$("${tools}/bin/kubectl" version --client --output=json 2>/dev/null || true)"
case "${kubectl_version}" in *\"gitVersion\":\"v1.33.3\"*) kubectl_ready=1 ;; *) kubectl_ready=0 ;; esac
if [ "${kubectl_ready}" != "1" ]; then
curl -fsSL -o "${tools}/bin/kubectl.tmp" https://dl.k8s.io/release/v1.33.3/bin/linux/arm64/kubectl
printf '%s %s\n' 3d514dbae5dc8c09f773df0ef0f5d449dfad05b3aca5c96b13565f886df345fd "${tools}/bin/kubectl.tmp" | sha256sum -c -
chmod 0755 "${tools}/bin/kubectl.tmp"
mv "${tools}/bin/kubectl.tmp" "${tools}/bin/kubectl"
fi
/bin/sh /opt/coordinator/install_agent_tools.sh
HOME=/opt/data/home git config --global user.name "Hermes Agent"
HOME=/opt/data/home git config --global user.email "hermes@bstein.dev"
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: home
mountPath: /opt/data
- name: coordinator
mountPath: /opt/coordinator
readOnly: true
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: "1", memory: 1Gi}
- name: repair-cassandra-kanban
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/repair_cassandra_kanban.py
- --database
- /opt/data/kanban/boards/cassandra/kanban.db
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 250m, memory: 256Mi}
- name: recover-cassandra-workers
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/recover_cassandra_workers.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 250m, memory: 256Mi}
- name: patch-auth
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/patch_hermes_auth.py
- /opt/hermes/hermes_cli/auth.py
- /patched/auth.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: coordinator
mountPath: /opt/coordinator
readOnly: true
- name: auth-patch
mountPath: /patched
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: patch-tui-gateway
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/patch_tui_gateway.py
- /opt/hermes/tui_gateway/server.py
- /patched/server.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: coordinator
mountPath: /opt/coordinator
readOnly: true
- name: tui-gateway-patch
mountPath: /patched
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: patch-api-server-sessions
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/patch_api_server_sessions.py
- /opt/hermes/gateway/platforms/api_server.py
- /patched/api_server.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: coordinator
mountPath: /opt/coordinator
readOnly: true
- name: api-server-patch
mountPath: /patched
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: patch-web-session-activity
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/patch_web_session_activity.py
- /opt/hermes/hermes_cli/web_server.py
- /patched/web_server.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: coordinator
mountPath: /opt/coordinator
readOnly: true
- name: web-server-patch
mountPath: /patched
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: patch-subprocess-secret-boundary
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
/opt/hermes/.venv/bin/python /opt/coordinator/patch_subprocess_secret_boundary.py \
/opt/hermes/tools/environments/local.py /patched/local.py
/opt/hermes/.venv/bin/python /opt/coordinator/patch_process_output_redaction.py \
/opt/hermes/tools/process_registry.py /patched/process_registry.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: subprocess-secret-patch, mountPath: /patched}
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: patch-codex-runtime
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/patch_codex_runtime.py
- /opt/hermes/hermes_cli/runtime_provider.py
- /patched/runtime_provider.py
- /opt/hermes/agent/transports/codex_app_server_session.py
- /patched/codex_app_server_session.py
- /opt/hermes/agent/codex_runtime.py
- /patched/codex_runtime.py
- /opt/hermes/agent/chat_completion_helpers.py
- /patched/chat_completion_helpers.py
- /opt/hermes/agent/conversation_loop.py
- /patched/conversation_loop.py
- /opt/hermes/agent/auxiliary_client.py
- /patched/auxiliary_client.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: coordinator
mountPath: /opt/coordinator
readOnly: true
- name: codex-runtime-patch
mountPath: /patched
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: bootstrap-coordinator
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/hermes_coordinator.py
- --once
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
- {name: PATH, value: '/opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin'}
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
- {name: routing-catalog, mountPath: /routing-catalog}
resources:
requests: {cpu: 50m, memory: 128Mi}
limits: {cpu: 500m, memory: 512Mi}
- name: configure-agent-clients
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- sh
- -ec
- |
set -a
. /opt/data/.env
set +a
/opt/hermes/.venv/bin/python /opt/coordinator/configure_agent_clients.py
/opt/hermes/.venv/bin/python /opt/coordinator/migrate_herdr_state.py
/opt/hermes/.venv/bin/python /opt/coordinator/migrate_api_session_lineage.py
# Client configuration restores the persisted Codex CLI login.
# Refresh routing afterwards so AUTO sees the app-server lane on
# the first request instead of waiting for the hourly steward.
/opt/hermes/.venv/bin/python /opt/coordinator/hermes_coordinator.py --once
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
- {name: PATH, value: '/opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin'}
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
- {name: routing-catalog, mountPath: /routing-catalog}
resources:
requests: {cpu: 25m, memory: 128Mi}
limits: {cpu: 500m, memory: 512Mi}
- name: prepare-ttyd-index
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/patch_ttyd_index.py
- /opt/data/tools/bin/ttyd
- /ttyd-index/index.html
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: ttyd-index, mountPath: /ttyd-index}
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 250m, memory: 128Mi}
containers:
- name: hermes
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
set -a
. /opt/data/.env
set +a
API_SERVER_KEY="$(tr -d '\r\n' < /runtime-access/agent-api-key)"
test -n "${API_SERVER_KEY}"
export API_SERVER_KEY
exec /init /opt/hermes/docker/main-wrapper.sh gateway run
ports:
- {name: api, containerPort: 8642, protocol: TCP}
- {name: dashboard, containerPort: 9119, protocol: TCP}
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
- {name: HERMES_CLAUDE_BIN, value: /opt/coordinator/claude}
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
- {name: PATH, value: '/opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/opt/hermes/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'}
- {name: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright}
- {name: AGENT_BROWSER_EXECUTABLE_PATH, value: /opt/hermes/.playwright/chromium_headless_shell-1228/chrome-linux/headless_shell}
- {name: AGENT_BROWSER_ARGS, value: "--no-sandbox,--disable-dev-shm-usage"}
- {name: HERMES_DASHBOARD, value: "1"}
- {name: HERMES_DASHBOARD_HOST, value: 127.0.0.1}
- {name: HERMES_DASHBOARD_PORT, value: "9119"}
- {name: HERMES_DASHBOARD_FILES_ROOT, value: /opt/data/workspace}
- {name: HERMES_DASHBOARD_PUBLIC_URL, value: 'https://worker.bstein.dev'}
- {name: HERMES_TUI_AGENT_INIT_TIMEOUT_S, value: "180"}
- {name: HERMES_STREAM_STALE_TIMEOUT, value: "600"}
- {name: HERMES_API_CALL_STALE_TIMEOUT, value: "600"}
- {name: API_SERVER_ENABLED, value: "true"}
- {name: API_SERVER_HOST, value: 0.0.0.0}
- {name: API_SERVER_PORT, value: "8642"}
# Comma-separated; the legacy origin stays until agent.hermes.bstein.dev is retired.
- {name: API_SERVER_CORS_ORIGINS, value: "https://worker.bstein.dev,https://agent.hermes.bstein.dev"}
- {name: HERMES_MEDIA_DELIVERY_STRICT, value: "1"}
- {name: HERMES_MEDIA_ALLOW_DIRS, value: /opt/data/workspace}
- {name: HERMES_AUTO_ROUTER_PROFILE, value: agent}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
- {name: HERMES_CODEX_HEALTH_PATH, value: /opt/data/provider-health/codex.json}
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
- {name: HERMES_IMAGE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
# Worker HUX hook wiring (observe-only). The hux-runtime plugin is
# mounted but stays OUT of plugins.enabled until the activation
# commit (docs/hux/WORKER-PLAN.md), so this environment is inert
# until that deliberate flip. Enforcement is pinned to 0: the hook
# observes and emits, it never gates worker tools in this stage.
- {name: HUX_BASE_URL, value: 'http://127.0.0.1:8790'}
- {name: HUX_RUNTIME_ENABLED, value: "1"}
- {name: HUX_TOOL_ENFORCEMENT, value: "0"}
- {name: HUX_TENANT_SLOT, value: slot-100}
- {name: HUX_WORKER_KEY_FILE, value: /run/hermes-hux-worker/worker-key}
- {name: HUX_SUBJECT_FILE, value: /run/hermes-hux-subject/subject}
- {name: HUX_CONTEXT_KEY_FILE, value: /run/hermes-hux-context/context-key}
- {name: HUX_PROJECT_SOURCE, value: 'profile:default'}
- {name: HUX_TIMEOUT_SECONDS, value: "3"}
volumeMounts:
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
- {name: web-server-patch, mountPath: /opt/hermes/hermes_cli/web_server.py, subPath: web_server.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/transports/codex_app_server_session.py, subPath: codex_app_server_session.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/chat_completion_helpers.py, subPath: chat_completion_helpers.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
- {name: api-server-patch, mountPath: /opt/hermes/gateway/platforms/api_server.py, subPath: api_server.py}
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/environments/local.py, subPath: local.py}
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/process_registry.py, subPath: process_registry.py}
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
- {name: atlas-pr-skill, mountPath: /opt/data/workspace/skills/manage-atlas-pull-requests, readOnly: true}
- {name: scm-boundary, mountPath: /opt/scm, readOnly: true}
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
- {name: tmp, mountPath: /tmp}
- {name: hux-runtime-plugin, mountPath: /opt/data/plugins/hux-runtime, readOnly: true}
- {name: hux-worker-key, mountPath: /run/hermes-hux-worker, readOnly: true}
- {name: home, mountPath: /run/hermes-hux-context, subPath: hux/context, readOnly: true}
- {name: home, mountPath: /run/hermes-hux-subject, subPath: hux/binding, readOnly: true}
startupProbe:
exec:
command: [curl, -fsS, 'http://127.0.0.1:9119/api/status']
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 60
readinessProbe:
exec:
command: [curl, -fsS, 'http://127.0.0.1:9119/api/status']
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command: [curl, -fsS, 'http://127.0.0.1:9119/api/status']
initialDelaySeconds: 90
periodSeconds: 30
timeoutSeconds: 15
failureThreshold: 5
securityContext:
allowPrivilegeEscalation: false
runAsUser: 0
runAsGroup: 0
seccompProfile:
type: RuntimeDefault
resources:
requests: {cpu: 125m, memory: 320Mi}
limits: {cpu: "3", memory: 6Gi}
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561
imagePullPolicy: IfNotPresent
args:
- --provider=oidc
- --config=/oauth2-config/oidc-config
# Single canonical callback. The __Host- cookie prefix below binds both the
# session and the CSRF cookie to one host, so a fresh login must start on
# worker.bstein.dev; agent.hermes.bstein.dev keeps serving already
# authenticated sessions until it is retired separately.
- --redirect-url=https://worker.bstein.dev/oauth2/callback
- --oidc-issuer-url=https://sso.bstein.dev/realms/atlas
- --user-id-claim=sub
- --oidc-groups-claim=groups
- --allowed-group=/hermes-owner
- --code-challenge-method=S256
- --scope=openid profile email
- --email-domain=*
- --authenticated-emails-file=/etc/oauth2-proxy/allowed-emails
- --custom-templates-dir=/etc/oauth2-proxy/templates
- --set-xauthrequest=true
- --pass-user-headers=true
- --pass-basic-auth=false
- --pass-host-header=false
- --proxy-websockets=true
- --cookie-name=__Host-hermes_agent
- --cookie-path=/
- --cookie-secure=true
- --cookie-samesite=lax
- --cookie-csrf-expire=10m
- --cookie-csrf-per-request=true
- --cookie-csrf-per-request-limit=8
- --cookie-refresh=19m
- --cookie-expire=8h
- --session-store-type=redis
- --redis-connection-url=redis://hermes-oauth-sessions.hermes.svc.cluster.local:6379/0
- --upstream=http://127.0.0.1:7681/terminal/
- --upstream=http://127.0.0.1:9119/
- --http-address=0.0.0.0:4180
- --skip-provider-button=true
- --reverse-proxy=true
- --api-route=^/api/
- --trusted-proxy-ip=10.42.0.0/16
ports:
- {name: auth-http, containerPort: 4180, protocol: TCP}
readinessProbe:
httpGet: {path: /ping, port: auth-http}
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: {path: /ping, port: auth-http}
initialDelaySeconds: 20
periodSeconds: 20
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 250m, memory: 256Mi}
volumeMounts:
- {name: allowlist, mountPath: /etc/oauth2-proxy, readOnly: true}
- {name: oauth2-config, mountPath: /oauth2-config, readOnly: true}
- {name: oauth-templates, mountPath: /etc/oauth2-proxy/templates, readOnly: true}
- {name: oauth-tmp, mountPath: /tmp}
- name: terminal
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
set -a
. /opt/data/.env
set +a
exec /opt/data/tools/bin/ttyd \
--writable \
--base-path /terminal \
--interface 0.0.0.0 \
--port 7681 \
--cwd /opt/data/workspace \
--terminal-type xterm-256color \
--index /ttyd-index/index.html \
--client-option disableReconnect=true \
--client-option "titleFixed=Hermes Agent" \
--client-option fontSize=15 \
/usr/bin/tmux new-session -A \
-s hermes-coordinator \
-c /opt/data/workspace \
/bin/sh -lc '
while true; do
/opt/hermes/.venv/bin/hermes \
--continue \
--tui \
--yolo \
--accept-hooks
exit_code=$?
printf "\r\nHermes exited (exit %s); resuming in 2 seconds...\r\n" "${exit_code}"
sleep 2
done
'
ports:
- {name: terminal, containerPort: 7681, protocol: TCP}
env:
- {name: HOME, value: /opt/data/home}
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
- {name: HERMES_CLAUDE_BIN, value: /opt/coordinator/claude}
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: PATH, value: '/opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin'}
- {name: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright}
- {name: AGENT_BROWSER_EXECUTABLE_PATH, value: /opt/hermes/.playwright/chromium_headless_shell-1228/chrome-linux/headless_shell}
- {name: AGENT_BROWSER_ARGS, value: "--no-sandbox,--disable-dev-shm-usage"}
- {name: HERMES_TUI_AGENT_INIT_TIMEOUT_S, value: "180"}
- {name: HERMES_AUTO_ROUTER_PROFILE, value: agent}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
- {name: HERMES_CODEX_HEALTH_PATH, value: /opt/data/provider-health/codex.json}
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/transports/codex_app_server_session.py, subPath: codex_app_server_session.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/chat_completion_helpers.py, subPath: chat_completion_helpers.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/environments/local.py, subPath: local.py}
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/process_registry.py, subPath: process_registry.py}
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
- {name: atlas-pr-skill, mountPath: /opt/data/workspace/skills/manage-atlas-pull-requests, readOnly: true}
- {name: scm-boundary, mountPath: /opt/scm, readOnly: true}
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
- {name: tmp, mountPath: /tmp}
- {name: ttyd-index, mountPath: /ttyd-index, readOnly: true}
startupProbe:
tcpSocket: {port: terminal}
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 60
readinessProbe:
tcpSocket: {port: terminal}
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
tcpSocket: {port: terminal}
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 500m, memory: 512Mi}
- name: cli-lane-runner
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
set -a
. /opt/data/.env
set +a
exec /opt/hermes/.venv/bin/python /opt/coordinator/cli_lane_runner.py
ports:
- {name: lane-metrics, containerPort: 9011, protocol: TCP}
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
- {name: HERMES_CLAUDE_BIN, value: /opt/coordinator/claude}
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: HERMES_CLI_LANE_CONCURRENCY, value: "2"}
- {name: HERMES_CLI_HEALTH_MAX_AGE_SECONDS, value: "60"}
- {name: HERMES_CLI_LANE_METRICS_PORT, value: "9011"}
- {name: HERMES_CLI_QUOTA_METRICS_URL, value: "http://127.0.0.1:9010/metrics"}
- {name: HERMES_AUTO_ROUTER_PROFILE, value: agent}
- {name: PATH, value: '/opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin'}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
- {name: atlas-pr-skill, mountPath: /opt/data/workspace/skills/manage-atlas-pull-requests, readOnly: true}
- {name: scm-boundary, mountPath: /opt/scm, readOnly: true}
- {name: tmp, mountPath: /tmp}
readinessProbe:
httpGet:
path: /healthz
port: lane-metrics
initialDelaySeconds: 2
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 3
resources:
requests: {cpu: 25m, memory: 96Mi}
limits: {cpu: "2", memory: 6Gi}
- name: model-steward
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"]
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
- {name: PATH, value: '/opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin'}
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
- {name: routing-catalog, mountPath: /routing-catalog}
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 250m, memory: 512Mi}
- name: kanban-supervisor
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
# Autonomous cross-card review->repair->re-review driver. Reads/writes
# only the local Kanban DB under /opt/data; creates Kanban cards that
# route through the existing subscription lanes. No runtime-access
# mount and no provider client: it holds no metered/API-key path. Inert
# until kanban.auto_supervise is set true in the deployed config.
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/kanban_supervisor.py]
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HOME, value: /opt/data/home}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: PYTHONDONTWRITEBYTECODE, value: "1"}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
resources:
requests: {cpu: 10m, memory: 32Mi}
limits: {cpu: 250m, memory: 256Mi}
- name: credential-sync
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/sync_runtime_credentials.py
env:
- {name: HERMES_CREDENTIAL_SYNC_VAULT_ROLE, value: hermes-credential-sync}
- {name: HERMES_CREDENTIAL_SYNC_INTERVAL, value: "60"}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
# Reconcile provider rotations to Vault and restore runtime files if a
# client leaves an incomplete credential behind after refresh.
- {name: runtime-access, mountPath: /runtime-access/claude, subPath: claude}
- {name: runtime-access, mountPath: /runtime-access/codex, subPath: codex}
resources:
requests: {cpu: 10m, memory: 32Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: ai-usage-exporter
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/ai_usage_exporter.py
ports:
- {name: ai-metrics, containerPort: 9010, protocol: TCP}
env:
- {name: HOME, value: /tmp}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: ATLAS_AI_CODEX_BIN, value: /opt/data/tools/bin/codex}
- {name: ATLAS_AI_CODEX_QUERY_TIMEOUT_SECONDS, value: "45"}
- {name: ATLAS_AI_CLAUDE_CREDENTIALS, value: /runtime-access/claude/.credentials.json}
- {name: ATLAS_AI_CLAUDE_OAUTH_TOKEN_FILE, value: /claude-oauth-access/token}
- {name: ATLAS_AI_CLAUDE_QUERY_TIMEOUT_SECONDS, value: "30"}
- {name: ATLAS_AI_CLAUDE_QUOTA_MODEL, value: claude-haiku-4-5-20251001}
- {name: ATLAS_AI_CLAUDE_USAGE_URL, value: 'https://api.anthropic.com/api/oauth/usage'}
- {name: ATLAS_AI_AUTHENTICATION_GRACE_SECONDS, value: "1200"}
- {name: ATLAS_AI_PROVIDER_HEALTH_ROOT, value: /provider-health}
- {name: ATLAS_AI_USAGE_INTERVAL_SECONDS, value: "300"}
- {name: ATLAS_AI_USAGE_PORT, value: "9010"}
readinessProbe:
httpGet: {path: /healthz, port: ai-metrics}
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: {path: /healthz, port: ai-metrics}
initialDelaySeconds: 30
periodSeconds: 30
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: home, mountPath: /opt/data/tools, subPath: tools, readOnly: true}
- {name: home, mountPath: /provider-health, subPath: provider-health, readOnly: true}
- {name: runtime-access, mountPath: /runtime-access/claude, subPath: claude}
- {name: runtime-access, mountPath: /runtime-access/codex, subPath: codex}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: tmp, mountPath: /tmp}
resources:
requests: {cpu: 10m, memory: 32Mi}
limits: {cpu: 250m, memory: 192Mi}
- name: image-broker
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
exec /opt/hermes/.venv/bin/python /opt/coordinator/image_broker.py
ports:
- {name: image-broker, containerPort: 9002, protocol: TCP}
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: HERMES_IMAGE_BROKER_DEFAULT_MODEL, value: atlas-image-auto-high}
- {name: HERMES_IMAGE_BROKER_LISTEN_PORT, value: "9002"}
- {name: HERMES_IMAGE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
- {name: HERMES_LOCAL_IMAGE_URL, value: 'http://hermes-local-image.hermes.svc.cluster.local:9004'}
- {name: HERMES_IMAGE_POLICY_PATH, value: /etc/hermes-image-policy/policy.json}
readinessProbe:
tcpSocket: {port: image-broker}
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket: {port: image-broker}
initialDelaySeconds: 30
periodSeconds: 30
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
- {name: image-policy, mountPath: /etc/hermes-image-policy, readOnly: true}
- {name: tmp, mountPath: /tmp}
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: "1", memory: 1Gi}
- name: codex-broker
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
exec /opt/hermes/.venv/bin/python /opt/coordinator/codex_broker.py
ports:
- {name: codex-broker, containerPort: 9003, protocol: TCP}
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: HERMES_CODEX_BROKER_LISTEN_PORT, value: "9003"}
- {name: HERMES_CODEX_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
- {name: HERMES_CODEX_HEALTH_PATH, value: /opt/data/provider-health/codex.json}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
readinessProbe:
tcpSocket: {port: codex-broker}
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket: {port: codex-broker}
initialDelaySeconds: 30
periodSeconds: 30
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
- {name: tmp, mountPath: /tmp}
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: "1", memory: 1Gi}
- name: claude-broker
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
unset ANTHROPIC_API_KEY CLAUDE_API_KEY
exec /opt/hermes/.venv/bin/python /opt/coordinator/claude_oauth_broker.py
ports:
- {name: claude-broker, containerPort: 9006, protocol: TCP}
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /runtime-access/codex}
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: PATH, value: '/opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin'}
- {name: HERMES_CLAUDE_BIN, value: /opt/coordinator/claude}
- {name: HERMES_CLAUDE_BROKER_PORT, value: "9006"}
- {name: HERMES_CLAUDE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
- {name: HERMES_CLAUDE_BROKER_READ_TIMEOUT, value: "1800"}
- {name: HERMES_CLAUDE_BROKER_CONCURRENCY, value: "2"}
- {name: HERMES_CLAUDE_HEALTH_POLL_SECONDS, value: "300"}
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
readinessProbe:
tcpSocket: {port: claude-broker}
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket: {port: claude-broker}
initialDelaySeconds: 30
periodSeconds: 30
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: runtime-access, mountPath: /runtime-access}
- {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
- {name: tmp, mountPath: /tmp}
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
resources:
requests: {cpu: 50m, memory: 128Mi}
limits: {cpu: "3", memory: 3Gi}
# Worker HUX service: the same reviewed WebUI image line as the chat
# tenants (one artifact, one Flux image policy), loopback-only, with
# foundation as the only enabled card until the Worker lifecycle gates
# in docs/hux/WORKER-PLAN.md pass on this instance.
- name: hux
image: registry.bstein.dev/bstein/hermes-webui:git-5b1f8320729ebd1514ce83b7e695a02938e5cef8-build-36-release@sha256:dfdacf006af93add1bd811637c30f3d4fd16b6f7e24d81ddc56d2989aa31210a # {"$imagepolicy": "hermes:hermes-webui-release"}
imagePullPolicy: IfNotPresent
command: [/opt/hermes/.venv/bin/python, -m, hux.server]
ports:
- {name: hux-loopback, containerPort: 8790, protocol: TCP}
env:
- {name: PYTHONPATH, value: /opt/hermes-hux}
- {name: PYTHONDONTWRITEBYTECODE, value: "1"}
- {name: HOME, value: /tmp}
- {name: HUX_BIND, value: 127.0.0.1}
- {name: HUX_PORT, value: "8790"}
- {name: HUX_TENANT_SLOT, value: slot-100}
- {name: HUX_DATA_ROOT, value: /var/lib/hux/store}
- {name: HUX_FLAGS, value: hux.foundation}
- {name: HUX_WORKER_KEY_FILE, value: /run/hermes-hux-worker/worker-key}
- {name: HUX_SUBJECT_BINDING_FILE, value: /var/lib/hux/binding/subject}
- {name: HUX_CONTEXT_KEY_FILE, value: /var/lib/hux/context/context-key}
- {name: HUX_CANARY_FILE, value: /var/lib/hux/context/redaction-canary}
- name: HUX_IMAGE_TAG
value: git-5b1f8320729ebd1514ce83b7e695a02938e5cef8-build-36-release # {"$imagepolicy": "hermes:hermes-webui-release:tag"}
- name: HUX_IMAGE_DIGEST
value: sha256:dfdacf006af93add1bd811637c30f3d4fd16b6f7e24d81ddc56d2989aa31210a # {"$imagepolicy": "hermes:hermes-webui-release:digest"}
- {name: HUX_READS_PER_MINUTE, value: "600"}
- {name: HUX_WRITES_PER_MINUTE, value: "120"}
- {name: HUX_REQUEST_TIMEOUT_SECONDS, value: "10"}
volumeMounts:
- {name: home, mountPath: /var/lib/hux, subPath: hux}
- {name: hux-worker-key, mountPath: /run/hermes-hux-worker, readOnly: true}
- {name: hux-tmp, mountPath: /tmp}
readinessProbe:
exec:
command:
- /opt/hermes/.venv/bin/python
- -c
- "import json,urllib.request; body=json.load(urllib.request.urlopen('http://127.0.0.1:8790/healthz', timeout=2)); assert body['status']=='ok'"
initialDelaySeconds: 2
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 12
livenessProbe:
exec:
command:
- /opt/hermes/.venv/bin/python
- -c
- "import json,urllib.request; body=json.load(urllib.request.urlopen('http://127.0.0.1:8790/healthz', timeout=2)); assert body['status']=='ok'"
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 250m, memory: 256Mi}
volumes:
- name: home
persistentVolumeClaim:
claimName: hermes-agent-home
- name: runtime-access
emptyDir:
medium: Memory
sizeLimit: 128Mi
- name: claude-oauth-access
emptyDir:
medium: Memory
sizeLimit: 1Mi
- name: routing-catalog
persistentVolumeClaim:
claimName: hermes-routing-catalog
- name: config
configMap:
name: hermes-agent-config
- name: coordinator
configMap:
name: hermes-coordinator
defaultMode: 0555
- name: kubeconfig
configMap:
name: hermes-agent-kubeconfig
defaultMode: 0444
- name: auth-patch
emptyDir: {}
- name: tui-gateway-patch
emptyDir: {}
- name: api-server-patch
emptyDir: {}
- name: web-server-patch
emptyDir: {}
- name: subprocess-secret-patch
emptyDir: {}
- name: codex-runtime-patch
emptyDir: {}
- name: auto-router-plugin
configMap:
name: hermes-auto-router-plugin
items:
- {key: __init__.py, path: __init__.py}
- {key: provider_status.py, path: provider_status.py}
- {key: plugin.yaml, path: plugin.yaml}
- {key: dashboard-manifest.json, path: dashboard/manifest.json}
- {key: dashboard-api.py, path: dashboard/plugin_api.py}
- {key: dashboard-index.js, path: dashboard/dist/index.js}
- {key: dashboard-style.css, path: dashboard/dist/style.css}
- name: atlas-pr-skill
configMap:
name: hermes-atlas-pr-skill
items:
- {key: SKILL.md, path: SKILL.md}
- {key: openai.yaml, path: agents/openai.yaml}
- name: scm-boundary
configMap:
name: hermes-scm-boundary-v2
defaultMode: 0555
- name: image-policy
configMap:
name: hermes-image-policy
- name: tmp
emptyDir:
sizeLimit: 4Gi
- name: oauth-tmp
emptyDir:
sizeLimit: 64Mi
- name: oauth2-config
emptyDir:
medium: Memory
sizeLimit: 1Mi
- name: allowlist
configMap:
name: hermes-owner-allowlist
- name: oauth-templates
configMap:
name: hermes-chat-oauth-templates
- name: ttyd-index
emptyDir:
sizeLimit: 2Mi
- name: hux-worker-key
emptyDir:
medium: Memory
sizeLimit: 1Mi
- name: hux-tmp
emptyDir:
sizeLimit: 64Mi
- name: hux-runtime-plugin
configMap:
name: hermes-hux-runtime-plugin
items:
- {key: __init__.py, path: __init__.py}
- {key: context_ids.py, path: context_ids.py}
- {key: emitters.py, path: emitters.py}
- {key: runtime.py, path: runtime.py}
- {key: tool_policy.py, path: tool_policy.py}
- {key: plugin.yaml, path: plugin.yaml}
- {key: hux-hook-init.py, path: hux_hook/__init__.py}
- {key: hux-hook-client.py, path: hux_hook/client.py}
- {key: hux-hook-hooks.py, path: hux_hook/hooks.py}