diff --git a/services/hermes/chat-configmap.yaml b/services/hermes/chat-configmap.yaml index 1ac6e20b..79a817e6 100644 --- a/services/hermes/chat-configmap.yaml +++ b/services/hermes/chat-configmap.yaml @@ -44,6 +44,7 @@ data: enabled: - atlas-broker - auto-router + - hux-runtime model_catalog: enabled: true ttl_hours: 1 diff --git a/services/hermes/chat-pvcs.yaml b/services/hermes/chat-pvcs.yaml index 50d286cb..553a8a3e 100644 --- a/services/hermes/chat-pvcs.yaml +++ b/services/hermes/chat-pvcs.yaml @@ -15,6 +15,24 @@ spec: requests: storage: 10Gi --- +# HUX tenants share the backing claim but only receive their pod-specific +# subdirectory through kubelet subPath mounts. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: hermes-chat-hux-data + namespace: hermes + labels: + app: hermes-chat-tenant + ai.bstein.dev/data: tenant-hux-ledger +spec: + accessModes: + - ReadWriteMany + storageClassName: astreae + resources: + requests: + storage: 10Gi +--- apiVersion: v1 kind: PersistentVolumeClaim metadata: diff --git a/services/hermes/chat-statefulset.yaml b/services/hermes/chat-statefulset.yaml index 374882ea..001c20c9 100644 --- a/services/hermes/chat-statefulset.yaml +++ b/services/hermes/chat-statefulset.yaml @@ -17,6 +17,8 @@ spec: whenScaled: Retain updateStrategy: type: RollingUpdate + rollingUpdate: + partition: 3 selector: matchLabels: app: hermes-chat-tenant @@ -30,6 +32,7 @@ spec: ai.bstein.dev/isolation: one Hermes process and PVC per Keycloak subject ai.bstein.dev/model-policy: uniform automatic policy with per-user overrides ai.bstein.dev/config-rev: "20260816-telegram-topics" + ai.bstein.dev/hux-config-rev: "20260824-hux-v1" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: hermes-chat vault.hashicorp.com/agent-inject-secret-chat-relay-key: kv/data/atlas/hermes/chat-telegram @@ -144,6 +147,152 @@ spec: resources: requests: {cpu: 25m, memory: 32Mi} limits: {cpu: 100m, memory: 64Mi} + - name: init-hux-runtime + image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107 + imagePullPolicy: IfNotPresent + command: [/bin/sh, -ec] + args: + - | + umask 077 + tenant_root="/hux-data/${HOSTNAME}" + export HUX_INIT_ROOT="${tenant_root}" + mkdir -p \ + "${tenant_root}/binding" \ + "${tenant_root}/context" \ + "${tenant_root}/store" \ + /hux-relay \ + /hux-worker + chown 10000:10000 \ + "${tenant_root}" \ + "${tenant_root}/binding" \ + "${tenant_root}/context" \ + "${tenant_root}/store" \ + /hux-relay \ + /hux-worker + chmod 0700 \ + "${tenant_root}" \ + "${tenant_root}/binding" \ + "${tenant_root}/context" \ + "${tenant_root}/store" \ + /hux-relay \ + /hux-worker + if [ ! -e "${tenant_root}/context/context-key" ]; then + dd if=/dev/urandom of="${tenant_root}/context/.context-key.tmp" bs=32 count=1 2>/dev/null + chown 10000:10000 "${tenant_root}/context/.context-key.tmp" + chmod 0600 "${tenant_root}/context/.context-key.tmp" + mv "${tenant_root}/context/.context-key.tmp" "${tenant_root}/context/context-key" + fi + test "$(wc -c < "${tenant_root}/context/context-key")" -eq 32 + chown 10000:10000 "${tenant_root}/context/context-key" + chmod 0600 "${tenant_root}/context/context-key" + ordinal="${HOSTNAME##*-}" + HUX_INIT_SLOT="slot-${ordinal}" \ + /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 "${tenant_root}/context/redaction-canary" ]; then + dd if=/dev/urandom bs=32 count=1 2>/dev/null \ + | sha256sum | cut -d ' ' -f 1 \ + > "${tenant_root}/context/.redaction-canary.tmp" + chown 10000:10000 "${tenant_root}/context/.redaction-canary.tmp" + chmod 0400 "${tenant_root}/context/.redaction-canary.tmp" + mv "${tenant_root}/context/.redaction-canary.tmp" "${tenant_root}/context/redaction-canary" + fi + for target in /hux-relay/relay-key /hux-worker/worker-key; do + 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}" + done + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + add: [CHOWN, DAC_OVERRIDE, FOWNER] + runAsUser: 0 + runAsGroup: 0 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - {name: hux-data, mountPath: /hux-data} + - {name: hux-relay-key, mountPath: /hux-relay} + - {name: hux-worker-key, mountPath: /hux-worker} + resources: + requests: {cpu: 10m, memory: 16Mi} + limits: {cpu: 50m, memory: 32Mi} + - name: stage-hux-evidence + image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107 + imagePullPolicy: IfNotPresent + command: [/bin/sh, -ec] + args: + - | + # HUX-12 enablement is fail-closed: until the Vault annotation + # projects hux-evidence-key, nothing is staged and the evidence + # capability stays off. The key is staged only for the hux + # service and producer containers, never for hermes or webui. + if [ -s /vault/secrets/hux-evidence-key ]; then + umask 077 + tr -d '\r\n' < /vault/secrets/hux-evidence-key > /hux-evidence/.evidence-key.tmp + chown 10000:10000 /hux-evidence/.evidence-key.tmp + chmod 0400 /hux-evidence/.evidence-key.tmp + mv /hux-evidence/.evidence-key.tmp /hux-evidence/evidence-key + fi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + add: [CHOWN, DAC_OVERRIDE, FOWNER] + runAsUser: 0 + runAsGroup: 0 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - {name: hux-evidence-key, mountPath: /hux-evidence} + 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 @@ -267,10 +416,15 @@ spec: API_SERVER_KEY="$(tr -d '\r\n' < /runtime-access/chat-relay-key)" test -n "${API_SERVER_KEY}" export API_SERVER_KEY + export HUX_TENANT_SLOT="slot-${ordinal}" exec /opt/hermes/.venv/bin/hermes gateway run ports: - {name: api, containerPort: 8642, protocol: TCP} env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name - {name: HERMES_HOME, value: /opt/data} - {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json} - {name: HOME, value: /opt/data/home} @@ -288,6 +442,16 @@ spec: - {name: HERMES_IMAGE_BROKER_URL, value: 'http://hermes-image-broker.hermes.svc.cluster.local:9002'} - {name: HERMES_IMAGE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key} - {name: HERMES_AUTO_ROUTER_PROFILE, value: chat} + - {name: HUX_BASE_URL, value: 'http://127.0.0.1:8790'} + - {name: HUX_RUNTIME_ENABLED, value: "1"} + # First rollout is observe-only until approval parking/resume is + # connected to the upstream tool loop and proven live. + - {name: HUX_TOOL_ENFORCEMENT, value: "0"} + - {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: home, mountPath: /opt/data} - {name: workspace, mountPath: /opt/data/workspace} @@ -300,6 +464,10 @@ spec: - {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/process_registry.py, subPath: process_registry.py} - {name: image-plugin, mountPath: /opt/hermes/plugins/image_gen/atlas-broker, readOnly: true} - {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true} + - {name: hux-runtime-plugin, mountPath: /opt/data/plugins/hux-runtime, readOnly: true} + - {name: hux-worker-key, mountPath: /run/hermes-hux-worker, readOnly: true} + - {name: hux-data, mountPath: /run/hermes-hux-context, subPathExpr: $(POD_NAME)/context, readOnly: true} + - {name: hux-data, mountPath: /run/hermes-hux-subject, subPathExpr: $(POD_NAME)/binding, readOnly: true} readinessProbe: tcpSocket: {port: api} initialDelaySeconds: 30 @@ -333,6 +501,10 @@ spec: ports: - {name: webui, containerPort: 8787, protocol: TCP} env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name - {name: HERMES_HOME, value: /opt/data} - {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json} - {name: HOME, value: /opt/data/home} @@ -361,12 +533,16 @@ spec: - {name: HERMES_LOCAL_STT_COMMAND, value: "/opt/hermes/.venv/bin/python /opt/coordinator/hermes_stt_client.py {input_path} --output-dir {output_dir} --language {language} --model {model}"} - {name: HERMES_WEBUI_ATLAS_TTS_URL, value: 'http://hermes-tts.hermes.svc.cluster.local:9001/v1/audio/speech'} - {name: HERMES_WEBUI_ATLAS_TTS_STREAM_URL, value: 'http://hermes-tts.hermes.svc.cluster.local:9001/v1/audio/speech/stream'} + - {name: HUX_CONTEXT_KEY_FILE, value: /run/hermes-hux-context/context-key} + - {name: HUX_PROJECT_SOURCE, value: 'profile:default'} volumeMounts: - {name: home, mountPath: /opt/data} - {name: workspace, mountPath: /opt/data/workspace} - {name: runtime-access, mountPath: /runtime-access, readOnly: true} - {name: coordinator, mountPath: /opt/coordinator, readOnly: true} - {name: tmp, mountPath: /tmp} + - {name: hux-relay-key, mountPath: /run/hermes-webui-hux, readOnly: true} + - {name: hux-data, mountPath: /run/hermes-hux-context, subPathExpr: $(POD_NAME)/context, readOnly: true} readinessProbe: httpGet: {path: /health, port: webui} initialDelaySeconds: 10 @@ -389,6 +565,137 @@ spec: resources: requests: {cpu: 100m, memory: 224Mi} limits: {cpu: 750m, memory: 1Gi} + - name: hux + image: registry.bstein.dev/bstein/hermes-webui:git-2f535d3a30633c7f3d27c8020c5ca21c0d0f9503-build-21-release@sha256:e5b9b2fa8296a7b8a6065c63e7b288cd3305d70a3acc4312055b819bb5f64c04 # {"$imagepolicy": "hermes:hermes-webui-release"} + imagePullPolicy: IfNotPresent + command: [/bin/sh, -ec] + args: + - | + ordinal="${HOSTNAME##*-}" + export HUX_TENANT_SLOT="slot-${ordinal}" + exec /opt/hermes/.venv/bin/python -m hux.server + ports: + - {name: hux-loopback, containerPort: 8790, protocol: TCP} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - {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_DATA_ROOT, value: /var/lib/hux/store} + - {name: HUX_FLAGS, value: 'hux.foundation'} + - {name: HUX_RELAY_KEY_FILE, value: /run/hermes-webui-hux/relay-key} + - {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-2f535d3a30633c7f3d27c8020c5ca21c0d0f9503-build-21-release'} # {"$imagepolicy": "hermes:hermes-webui-release:tag"} + - {name: HUX_IMAGE_DIGEST, value: 'sha256:e5b9b2fa8296a7b8a6065c63e7b288cd3305d70a3acc4312055b819bb5f64c04'} # {"$imagepolicy": "hermes:hermes-webui-release:digest"} + - {name: HUX_SWITCHYARD_ROUTE_CATALOG, value: 'atlas/manual/codex/luna,atlas/manual/codex/terra,atlas/manual/codex/sol,atlas/manual/claude/haiku,atlas/manual/claude/fable,atlas/manual/claude/sonnet,atlas/manual/claude/opus,atlas/manual/local/qwen-14b'} + - {name: HUX_RELEASE_EVIDENCE_KEY_FILE, value: /run/hermes-hux-evidence/evidence-key} + - {name: HUX_RELEASE_EVIDENCE_POLICY_FILE, value: /etc/hux-evidence/policy.json} + - {name: HUX_READS_PER_MINUTE, value: "600"} + - {name: HUX_WRITES_PER_MINUTE, value: "120"} + - {name: HUX_REQUEST_TIMEOUT_SECONDS, value: "10"} + volumeMounts: + - {name: hux-data, mountPath: /var/lib/hux, subPathExpr: $(POD_NAME)} + - {name: hux-relay-key, mountPath: /run/hermes-webui-hux, readOnly: true} + - {name: hux-worker-key, mountPath: /run/hermes-hux-worker, readOnly: true} + - {name: hux-tmp, mountPath: /tmp} + - {name: hux-evidence-key, mountPath: /run/hermes-hux-evidence, readOnly: true} + - {name: hux-evidence-policy, mountPath: /etc/hux-evidence, readOnly: true} + 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: 2 + 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: 3 + 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} + - name: hux-evidence-producer + image: registry.bstein.dev/bstein/hermes-webui:git-2f535d3a30633c7f3d27c8020c5ca21c0d0f9503-build-21-release@sha256:e5b9b2fa8296a7b8a6065c63e7b288cd3305d70a3acc4312055b819bb5f64c04 # {"$imagepolicy": "hermes:hermes-webui-release"} + imagePullPolicy: IfNotPresent + command: [/bin/sh, -ec] + args: + - | + ordinal="${HOSTNAME##*-}" + export HUX_TENANT_SLOT="slot-${ordinal}" + while true; do + if [ -s /run/hermes-hux-evidence/evidence-key ] \ + && [ -s /run/hermes-hux-subject/subject ] \ + && [ -n "${HUX_PRODUCER_PROJECT_ID:-}" ] \ + && [ -n "${HUX_PRODUCER_CONVERSATION_ID:-}" ]; then + HUX_PRODUCER_SUBJECT="$(tr -d '\r\n' < /run/hermes-hux-subject/subject)" \ + /opt/hermes/.venv/bin/python -c \ + 'from hux_producer import run_once; run_once()' || true + fi + sleep 60 + done + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - {name: PYTHONPATH, value: /opt/hermes-hux} + - {name: PYTHONDONTWRITEBYTECODE, value: "1"} + - {name: HOME, value: /tmp} + - {name: HUX_BASE_URL, value: 'http://127.0.0.1:8790'} + - {name: HUX_PRODUCER_WORKLOAD, value: hermes-webui} + - {name: HUX_PRODUCER_NAMESPACE, value: hermes} + - {name: HUX_PRODUCER_POD_SELECTOR, value: 'app=hermes-chat-tenant'} + - {name: HUX_PRODUCER_WORKLOAD_KIND, value: statefulset} + - {name: HUX_PRODUCER_WORKLOAD_NAME, value: hermes-chat-tenant} + - {name: HUX_PRODUCER_TIMEOUT_SECONDS, value: "10"} + - {name: HUX_RELEASE_EVIDENCE_KEY_FILE, value: /run/hermes-hux-evidence/evidence-key} + - {name: HUX_RELEASE_EVIDENCE_POLICY_FILE, value: /etc/hux-evidence/policy.json} + envFrom: + - configMapRef: + name: hermes-hux-evidence-scope + optional: true + volumeMounts: + - {name: hux-evidence-key, mountPath: /run/hermes-hux-evidence, readOnly: true} + - {name: hux-evidence-policy, mountPath: /etc/hux-evidence, readOnly: true} + - {name: hux-data, mountPath: /run/hermes-hux-subject, subPathExpr: $(POD_NAME)/binding, readOnly: true} + - {name: hux-tmp, mountPath: /tmp} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: true + runAsUser: 10000 + runAsGroup: 10000 + seccompProfile: + type: RuntimeDefault + resources: + requests: {cpu: 10m, memory: 48Mi} + limits: {cpu: 100m, memory: 128Mi} - name: telegram-media image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107 imagePullPolicy: IfNotPresent @@ -452,9 +759,44 @@ spec: - name: image-plugin configMap: name: hermes-chat-image-plugin + - 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} - name: tmp emptyDir: sizeLimit: 256Mi + - name: hux-relay-key + emptyDir: + medium: Memory + sizeLimit: 1Mi + - name: hux-worker-key + emptyDir: + medium: Memory + sizeLimit: 1Mi + - name: hux-tmp + emptyDir: + sizeLimit: 64Mi + - name: hux-evidence-key + emptyDir: + medium: Memory + sizeLimit: 1Mi + - name: hux-evidence-policy + configMap: + name: hermes-hux-evidence-policy + defaultMode: 0444 + - name: hux-data + persistentVolumeClaim: + claimName: hermes-chat-hux-data volumeClaimTemplates: - metadata: name: home diff --git a/services/hermes/hux-evidence-policy.json b/services/hermes/hux-evidence-policy.json new file mode 100644 index 00000000..8b5fb1c4 --- /dev/null +++ b/services/hermes/hux-evidence-policy.json @@ -0,0 +1,13 @@ +{ + "schema": "hux.release_evidence_policy.v1", + "max_evidence_age_seconds": 900, + "workloads": { + "hermes-webui": { + "review_url_prefix": "https://scm.bstein.dev/atlas/titan-iac/pulls/", + "jenkins_job_url": "https://jenkins.bstein.dev/job/hermes-webui-image", + "image_repository": "registry.bstein.dev/bstein/hermes-webui", + "flux_kustomization": "hermes", + "health_url": "https://chat.bstein.dev/healthz" + } + } +} diff --git a/services/hermes/hux-evidence-rbac.yaml b/services/hermes/hux-evidence-rbac.yaml new file mode 100644 index 00000000..ed80d4d1 --- /dev/null +++ b/services/hermes/hux-evidence-rbac.yaml @@ -0,0 +1,54 @@ +# services/hermes/hux-evidence-rbac.yaml +# Read-only facts for the HUX-12 evidence producer sidecar: Ready pod +# imageIDs and the desired StatefulSet image in this namespace, plus the +# single named Flux Kustomization's applied revision. The pod service +# account gains nothing writable and nothing secret. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: hermes-hux-evidence-read + namespace: hermes +rules: + - apiGroups: [""] + resources: [pods] + verbs: [get, list] + - apiGroups: [apps] + resources: [statefulsets] + verbs: [get] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: hermes-hux-evidence-read + namespace: hermes +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: hermes-hux-evidence-read +subjects: + - kind: ServiceAccount + name: hermes-chat + namespace: hermes +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: hermes-hux-evidence-kustomization-read +rules: + - apiGroups: [kustomize.toolkit.fluxcd.io] + resources: [kustomizations] + resourceNames: [hermes] + verbs: [get] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: hermes-hux-evidence-kustomization-read +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: hermes-hux-evidence-kustomization-read +subjects: + - kind: ServiceAccount + name: hermes-chat + namespace: hermes diff --git a/services/hermes/kustomization.yaml b/services/hermes/kustomization.yaml index 4034b8f2..6647273d 100644 --- a/services/hermes/kustomization.yaml +++ b/services/hermes/kustomization.yaml @@ -43,6 +43,7 @@ resources: - agent-certificate.yaml - agent-ingress.yaml - execution-worker-rbac.yaml + - hux-evidence-rbac.yaml - execution-worker-statefulset.yaml - execution-mediator.yaml - execution-worker-networkpolicy.yaml @@ -196,6 +197,26 @@ configMapGenerator: - dashboard-style.css=plugins/auto-router/dashboard/dist/style.css options: disableNameSuffixHash: true + - name: hermes-hux-evidence-policy + namespace: hermes + files: + - policy.json=hux-evidence-policy.json + options: + disableNameSuffixHash: true + - name: hermes-hux-runtime-plugin + namespace: hermes + files: + - __init__.py=plugins/hux-runtime/__init__.py + - context_ids.py=plugins/hux-runtime/context_ids.py + - emitters.py=plugins/hux-runtime/emitters.py + - runtime.py=plugins/hux-runtime/runtime.py + - tool_policy.py=plugins/hux-runtime/tool_policy.py + - plugin.yaml=plugins/hux-runtime/plugin.yaml + - hux-hook-init.py=plugins/hux-runtime/hux_hook/__init__.py + - hux-hook-client.py=plugins/hux-runtime/hux_hook/client.py + - hux-hook-hooks.py=plugins/hux-runtime/hux_hook/hooks.py + options: + disableNameSuffixHash: true - name: hermes-chat-image-plugin namespace: hermes files: diff --git a/services/hermes/networkpolicy.yaml b/services/hermes/networkpolicy.yaml index fa6311f9..30f2e8d4 100644 --- a/services/hermes/networkpolicy.yaml +++ b/services/hermes/networkpolicy.yaml @@ -406,6 +406,22 @@ spec: ports: - {protocol: TCP, port: 9005} - {protocol: TCP, port: 9009} + # HUX-12 evidence producer: the Kubernetes API (ClusterIP) for pod and + # Flux facts, and the ingress edge for Jenkins/Harbor/health receipts. + - to: + - ipBlock: + cidr: 10.43.0.1/32 + ports: + - {protocol: TCP, port: 443} + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik + podSelector: + matchLabels: + app.kubernetes.io/name: traefik + ports: + - {protocol: TCP, port: 443} - to: - podSelector: matchLabels: diff --git a/services/hermes/oauth2-proxy.yaml b/services/hermes/oauth2-proxy.yaml index 5cd9d01f..f9e76a85 100644 --- a/services/hermes/oauth2-proxy.yaml +++ b/services/hermes/oauth2-proxy.yaml @@ -204,6 +204,7 @@ spec: - --redis-connection-url=redis://hermes-oauth-sessions.hermes.svc.cluster.local:6379/1 - --custom-templates-dir=/etc/oauth2-proxy/templates - '--skip-auth-route=GET=^/sw[.]js([?].*)?$' + - '--skip-auth-route=GET=^/healthz$' - --api-route=^/api/ - --api-route=^/health$ - --upstream=http://hermes-chat-router.hermes.svc.cluster.local:8080 diff --git a/testing/tests/test_hermes_hux_delivery.py b/testing/tests/test_hermes_hux_delivery.py index df32e030..33d35f54 100644 --- a/testing/tests/test_hermes_hux_delivery.py +++ b/testing/tests/test_hermes_hux_delivery.py @@ -95,7 +95,11 @@ def test_hux_sidecar_is_loopback_only_and_uses_the_reviewed_webui_image() -> Non assert values["HUX_BIND"] == "127.0.0.1" assert values["HUX_PORT"] == "8790" assert values["HUX_DATA_ROOT"] == "/var/lib/hux/store" - assert set(values["HUX_FLAGS"].split(",")) == ALL_FLAGS + # Progressive enablement: the canary starts foundation-only and cards are + # switched on one lifecycle gate at a time; unknown flags never ship. + flags = {flag for flag in values["HUX_FLAGS"].split(",") if flag} + assert "hux.foundation" in flags + assert flags <= ALL_FLAGS assert set(values["HUX_SWITCHYARD_ROUTE_CATALOG"].split(",")) == { "atlas/manual/codex/luna", "atlas/manual/codex/terra",