diff --git a/services/hermes/chat-configmap.yaml b/services/hermes/chat-configmap.yaml index 9ee5cf044..ab88fca7b 100644 --- a/services/hermes/chat-configmap.yaml +++ b/services/hermes/chat-configmap.yaml @@ -29,8 +29,8 @@ data: enabled: true ttl_hours: 1 platform_toolsets: - cli: [clarify, session_search, web] - api_server: [clarify, session_search, web] + cli: [clarify, file, memory, session_search, skills, todo, web] + api_server: [clarify, file, memory, session_search, skills, todo, web] dashboard: public_url: https://chat.hermes.bstein.dev display: @@ -43,15 +43,17 @@ data: with questions, writing, research, planning, and learning. Be direct, thoughtful, and careful. Use public web research when freshness matters. - This is a personal sandbox. Never attempt cluster administration, private - service access, repository modification, terminal execution, credentials, - or coordination of Brad's project agents. Those capabilities are not part - of this chat product. The user's conversations and files must never be - mixed with another Keycloak user's state. + This is a personal sandbox. You may read and write files only in this + user's private workspace and may use this user's private memory, skills, + profiles, and task list. Never attempt cluster administration, private + service access, terminal execution, credentials, or coordination of Brad's + project agents. The user's conversations and files must never be mixed with + another Keycloak user's state. AGENTS.md: | # Private Hermes chat This runtime belongs to one authenticated Keycloak identity and one private - persistent volume. Provide conversational help with clarify, session search, - and public web tools only. Do not claim access to Kubernetes, Vault, Gitea, - Brad's projects, other users, the agent coordinator, or automated triage. + persistent volume. Provide conversational help with the private workspace, + memory, skills, profiles, task list, session search, and public web tools. + Do not claim access to Kubernetes, Vault, Gitea, Brad's projects, other + users, the agent coordinator, or automated triage. diff --git a/services/hermes/chat-pvcs.yaml b/services/hermes/chat-pvcs.yaml new file mode 100644 index 000000000..50d286cbe --- /dev/null +++ b/services/hermes/chat-pvcs.yaml @@ -0,0 +1,61 @@ +# services/hermes/chat-pvcs.yaml +# Existing tenant claims are explicit so Flux can expand them with the template. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: home-hermes-chat-tenant-0 + namespace: hermes + labels: + app: hermes-chat-tenant +spec: + accessModes: + - ReadWriteOnce + storageClassName: astreae + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: home-hermes-chat-tenant-1 + namespace: hermes + labels: + app: hermes-chat-tenant +spec: + accessModes: + - ReadWriteOnce + storageClassName: astreae + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: home-hermes-chat-tenant-2 + namespace: hermes + labels: + app: hermes-chat-tenant +spec: + accessModes: + - ReadWriteOnce + storageClassName: astreae + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: home-hermes-chat-tenant-3 + namespace: hermes + labels: + app: hermes-chat-tenant +spec: + accessModes: + - ReadWriteOnce + storageClassName: astreae + resources: + requests: + storage: 10Gi diff --git a/services/hermes/chat-router.yaml b/services/hermes/chat-router.yaml index 046ebea2e..1f91ba137 100644 --- a/services/hermes/chat-router.yaml +++ b/services/hermes/chat-router.yaml @@ -20,7 +20,7 @@ spec: app: hermes-chat-router annotations: ai.bstein.dev/role: privacy-preserving-chat-tenant-router - ai.bstein.dev/config-rev: "20260808-webui-telegram-router-image" + ai.bstein.dev/config-rev: "20260808-tenant-personalization" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/agent-init-first: "true" @@ -62,7 +62,7 @@ spec: values: [rpi5] containers: - name: router - image: registry.bstein.dev/bstein/hermes-chat-router@sha256:ccb3a7c7ceba317153ff64455777459a5cfd9497bc0e6f51687feedfbfd6f4ed + image: registry.bstein.dev/bstein/hermes-chat-router@sha256:d6b938bbde0a229b73dd2f1fc5c4607ea9391b21278a2ae0e43c135b83587534 imagePullPolicy: IfNotPresent ports: - {name: http, containerPort: 8080, protocol: TCP} diff --git a/services/hermes/chat-statefulset.yaml b/services/hermes/chat-statefulset.yaml index 1b6bffa06..d30832a83 100644 --- a/services/hermes/chat-statefulset.yaml +++ b/services/hermes/chat-statefulset.yaml @@ -6,10 +6,15 @@ metadata: namespace: hermes labels: app: hermes-chat-tenant + annotations: + kustomize.toolkit.fluxcd.io/force: enabled spec: serviceName: hermes-chat-tenant replicas: 4 podManagementPolicy: Parallel + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Retain updateStrategy: type: RollingUpdate selector: @@ -116,6 +121,7 @@ spec: mv /opt/data/.env.tmp /opt/data/.env fi fi + rm -f /vault/secrets/anthropic-token /vault/secrets/chat-relay-key chmod 0600 /opt/data/.env chown -R 10000:10000 /opt/data securityContext: @@ -160,8 +166,10 @@ spec: - {name: api, containerPort: 8642, protocol: TCP} env: - {name: HERMES_HOME, value: /opt/data} - - {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json} + - {name: HERMES_AUTH_FILE, value: /opt/data/auth.json} - {name: HOME, value: /opt/data/home} + - {name: TERMINAL_CWD, value: /opt/data/workspace} + - {name: HERMES_WRITE_SAFE_ROOT, value: /opt/data/workspace} - {name: PATH, value: /opt/data/home/.local/bin:/opt/hermes/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin} - {name: HERMES_DASHBOARD, value: "0"} - {name: API_SERVER_ENABLED, value: "true"} @@ -170,7 +178,7 @@ spec: - {name: API_SERVER_CORS_ORIGINS, value: https://chat.hermes.bstein.dev} volumeMounts: - {name: home, mountPath: /opt/data} - - {name: provider-auth, mountPath: /shared-auth} + - {name: provider-auth, mountPath: /opt/data/auth.json, subPath: auth.json, readOnly: true} - {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py} readinessProbe: tcpSocket: {port: api} @@ -206,7 +214,7 @@ spec: - {name: webui, containerPort: 8787, protocol: TCP} env: - {name: HERMES_HOME, value: /opt/data} - - {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json} + - {name: HERMES_AUTH_FILE, value: /opt/data/auth.json} - {name: HOME, value: /opt/data/home} - {name: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes} - {name: HERMES_WEBUI_HOST, value: 0.0.0.0} @@ -223,7 +231,7 @@ spec: - {name: HERMES_WEBUI_TRUST_FORWARDED_PROTO, value: "1"} volumeMounts: - {name: home, mountPath: /opt/data} - - {name: provider-auth, mountPath: /shared-auth, readOnly: true} + - {name: provider-auth, mountPath: /opt/data/auth.json, subPath: auth.json, readOnly: true} - {name: tmp, mountPath: /tmp} readinessProbe: httpGet: {path: /health, port: webui} @@ -273,4 +281,4 @@ spec: storageClassName: astreae resources: requests: - storage: 2Gi + storage: 10Gi diff --git a/services/hermes/kustomization.yaml b/services/hermes/kustomization.yaml index c3a7fef32..37e54d65f 100644 --- a/services/hermes/kustomization.yaml +++ b/services/hermes/kustomization.yaml @@ -10,6 +10,7 @@ resources: - chat-configmap.yaml - rbac.yaml - pvc.yaml + - chat-pvcs.yaml - model-gate-rbac.yaml - ariadne-handoff-rbac.yaml - model-gate-state.yaml diff --git a/services/hermes/networkpolicy.yaml b/services/hermes/networkpolicy.yaml index 6f70516f4..e003d358a 100644 --- a/services/hermes/networkpolicy.yaml +++ b/services/hermes/networkpolicy.yaml @@ -45,7 +45,7 @@ spec: matchLabels: app: oauth2-proxy-hermes-triage ports: - - {protocol: TCP, port: 9119} + - {protocol: TCP, port: 8787} - from: - namespaceSelector: matchLabels: @@ -72,7 +72,7 @@ spec: matchLabels: app: oauth2-proxy-hermes-agent ports: - - {protocol: TCP, port: 9119} + - {protocol: TCP, port: 8787} egress: - to: - namespaceSelector: @@ -310,5 +310,5 @@ spec: operator: In values: [hermes, hermes-agent, hermes-chat-router] ports: - - {protocol: TCP, port: 9119} + - {protocol: TCP, port: 8787} - {protocol: TCP, port: 8080} diff --git a/services/hermes/oauth2-proxy.yaml b/services/hermes/oauth2-proxy.yaml index 238396244..a10a1cc8f 100644 --- a/services/hermes/oauth2-proxy.yaml +++ b/services/hermes/oauth2-proxy.yaml @@ -96,7 +96,7 @@ spec: - --cookie-samesite=lax - --cookie-refresh=1h - --cookie-expire=8h - - --upstream=http://hermes-agent.hermes.svc.cluster.local:9119 + - --upstream=http://hermes-agent.hermes.svc.cluster.local:8787 - --http-address=0.0.0.0:4180 - --skip-provider-button=true - --reverse-proxy=true @@ -186,7 +186,7 @@ spec: - --cookie-samesite=lax - --cookie-refresh=1h - --cookie-expire=8h - - --upstream=http://hermes-triage.hermes.svc.cluster.local:9119 + - --upstream=http://hermes-triage.hermes.svc.cluster.local:8787 - --http-address=0.0.0.0:4180 - --skip-provider-button=true - --reverse-proxy=true diff --git a/services/hermes/router/main.go b/services/hermes/router/main.go index 3a49f331d..1d908a6f5 100644 --- a/services/hermes/router/main.go +++ b/services/hermes/router/main.go @@ -50,9 +50,9 @@ var deniedPrefixes = []string{ "/api/cron", "/api/curator", "/api/dashboard/config", "/api/env", "/api/escape", "/api/extensions", "/api/file", "/api/folder", "/api/gateway", "/api/git", "/api/git-info", "/api/health/restart", "/api/kanban", "/api/logs", "/api/mcp", - "/api/memory", "/api/messaging", "/api/notes", "/api/ops", - "/api/oauth", "/api/onboarding/oauth", "/api/pairing", "/api/plugins", "/api/profiles", "/api/providers", - "/api/rollback", "/api/shutdown", "/api/skills", "/api/terminal", + "/api/messaging", "/api/notes", "/api/ops", + "/api/oauth", "/api/onboarding/oauth", "/api/pairing", "/api/plugins", "/api/providers", + "/api/rollback", "/api/shutdown", "/api/terminal", "/api/tools", "/api/updates", "/api/webhooks", "/api/wiki", "/api/workspace", "/api/workspaces", } diff --git a/services/hermes/router/main_test.go b/services/hermes/router/main_test.go index 5c13ed0e9..fb8e6ca7d 100644 --- a/services/hermes/router/main_test.go +++ b/services/hermes/router/main_test.go @@ -95,6 +95,33 @@ func TestRouterProxiesWebUIAndAddsTelegramShortcut(t *testing.T) { } } +func TestRouterAllowsTenantScopedPersonalization(t *testing.T) { + backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusNoContent) + })) + defer backend.Close() + router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL }) + if err != nil { + t.Fatal(err) + } + for _, path := range []string{ + "/api/memory", + "/api/memory/write", + "/api/skills", + "/api/skills/save", + "/api/profiles", + "/api/profile/create", + } { + request := httptest.NewRequest(http.MethodGet, path, nil) + request.Header.Set("X-Forwarded-User", "subject") + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + if response.Code != http.StatusNoContent { + t.Fatalf("GET %s: got %d, want %d", path, response.Code, http.StatusNoContent) + } + } +} + func TestWebUIModelAndReasoningOverridesAreProxied(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost { diff --git a/services/hermes/service.yaml b/services/hermes/service.yaml index 5b31ca7b3..871efbbaf 100644 --- a/services/hermes/service.yaml +++ b/services/hermes/service.yaml @@ -36,7 +36,7 @@ spec: port: 8642 targetPort: api - name: dashboard - port: 9119 + port: 8787 targetPort: dashboard --- apiVersion: v1 @@ -52,7 +52,7 @@ spec: app: hermes-agent ports: - name: dashboard - port: 9119 + port: 8787 targetPort: dashboard --- apiVersion: v1