fix(hermes): restore isolated user features
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 153

This commit is contained in:
jenkins 2026-08-08 21:28:33 -03:00
parent cd1fb62d0f
commit 5159c98d64
10 changed files with 126 additions and 27 deletions

View File

@ -29,8 +29,8 @@ data:
enabled: true enabled: true
ttl_hours: 1 ttl_hours: 1
platform_toolsets: platform_toolsets:
cli: [clarify, session_search, web] cli: [clarify, file, memory, session_search, skills, todo, web]
api_server: [clarify, session_search, web] api_server: [clarify, file, memory, session_search, skills, todo, web]
dashboard: dashboard:
public_url: https://chat.hermes.bstein.dev public_url: https://chat.hermes.bstein.dev
display: display:
@ -43,15 +43,17 @@ data:
with questions, writing, research, planning, and learning. Be direct, with questions, writing, research, planning, and learning. Be direct,
thoughtful, and careful. Use public web research when freshness matters. thoughtful, and careful. Use public web research when freshness matters.
This is a personal sandbox. Never attempt cluster administration, private This is a personal sandbox. You may read and write files only in this
service access, repository modification, terminal execution, credentials, user's private workspace and may use this user's private memory, skills,
or coordination of Brad's project agents. Those capabilities are not part profiles, and task list. Never attempt cluster administration, private
of this chat product. The user's conversations and files must never be service access, terminal execution, credentials, or coordination of Brad's
mixed with another Keycloak user's state. project agents. The user's conversations and files must never be mixed with
another Keycloak user's state.
AGENTS.md: | AGENTS.md: |
# Private Hermes chat # Private Hermes chat
This runtime belongs to one authenticated Keycloak identity and one private This runtime belongs to one authenticated Keycloak identity and one private
persistent volume. Provide conversational help with clarify, session search, persistent volume. Provide conversational help with the private workspace,
and public web tools only. Do not claim access to Kubernetes, Vault, Gitea, memory, skills, profiles, task list, session search, and public web tools.
Brad's projects, other users, the agent coordinator, or automated triage. Do not claim access to Kubernetes, Vault, Gitea, Brad's projects, other
users, the agent coordinator, or automated triage.

View File

@ -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

View File

@ -20,7 +20,7 @@ spec:
app: hermes-chat-router app: hermes-chat-router
annotations: annotations:
ai.bstein.dev/role: privacy-preserving-chat-tenant-router 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-inject: "true"
vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-init-first: "true" vault.hashicorp.com/agent-init-first: "true"
@ -62,7 +62,7 @@ spec:
values: [rpi5] values: [rpi5]
containers: containers:
- name: router - name: router
image: registry.bstein.dev/bstein/hermes-chat-router@sha256:ccb3a7c7ceba317153ff64455777459a5cfd9497bc0e6f51687feedfbfd6f4ed image: registry.bstein.dev/bstein/hermes-chat-router@sha256:d6b938bbde0a229b73dd2f1fc5c4607ea9391b21278a2ae0e43c135b83587534
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- {name: http, containerPort: 8080, protocol: TCP} - {name: http, containerPort: 8080, protocol: TCP}

View File

@ -6,10 +6,15 @@ metadata:
namespace: hermes namespace: hermes
labels: labels:
app: hermes-chat-tenant app: hermes-chat-tenant
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
spec: spec:
serviceName: hermes-chat-tenant serviceName: hermes-chat-tenant
replicas: 4 replicas: 4
podManagementPolicy: Parallel podManagementPolicy: Parallel
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
updateStrategy: updateStrategy:
type: RollingUpdate type: RollingUpdate
selector: selector:
@ -116,6 +121,7 @@ spec:
mv /opt/data/.env.tmp /opt/data/.env mv /opt/data/.env.tmp /opt/data/.env
fi fi
fi fi
rm -f /vault/secrets/anthropic-token /vault/secrets/chat-relay-key
chmod 0600 /opt/data/.env chmod 0600 /opt/data/.env
chown -R 10000:10000 /opt/data chown -R 10000:10000 /opt/data
securityContext: securityContext:
@ -160,8 +166,10 @@ spec:
- {name: api, containerPort: 8642, protocol: TCP} - {name: api, containerPort: 8642, protocol: TCP}
env: env:
- {name: HERMES_HOME, value: /opt/data} - {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: 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: 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: HERMES_DASHBOARD, value: "0"}
- {name: API_SERVER_ENABLED, value: "true"} - {name: API_SERVER_ENABLED, value: "true"}
@ -170,7 +178,7 @@ spec:
- {name: API_SERVER_CORS_ORIGINS, value: https://chat.hermes.bstein.dev} - {name: API_SERVER_CORS_ORIGINS, value: https://chat.hermes.bstein.dev}
volumeMounts: volumeMounts:
- {name: home, mountPath: /opt/data} - {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} - {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
readinessProbe: readinessProbe:
tcpSocket: {port: api} tcpSocket: {port: api}
@ -206,7 +214,7 @@ spec:
- {name: webui, containerPort: 8787, protocol: TCP} - {name: webui, containerPort: 8787, protocol: TCP}
env: env:
- {name: HERMES_HOME, value: /opt/data} - {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: HOME, value: /opt/data/home}
- {name: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes} - {name: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes}
- {name: HERMES_WEBUI_HOST, value: 0.0.0.0} - {name: HERMES_WEBUI_HOST, value: 0.0.0.0}
@ -223,7 +231,7 @@ spec:
- {name: HERMES_WEBUI_TRUST_FORWARDED_PROTO, value: "1"} - {name: HERMES_WEBUI_TRUST_FORWARDED_PROTO, value: "1"}
volumeMounts: volumeMounts:
- {name: home, mountPath: /opt/data} - {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} - {name: tmp, mountPath: /tmp}
readinessProbe: readinessProbe:
httpGet: {path: /health, port: webui} httpGet: {path: /health, port: webui}
@ -273,4 +281,4 @@ spec:
storageClassName: astreae storageClassName: astreae
resources: resources:
requests: requests:
storage: 2Gi storage: 10Gi

View File

@ -10,6 +10,7 @@ resources:
- chat-configmap.yaml - chat-configmap.yaml
- rbac.yaml - rbac.yaml
- pvc.yaml - pvc.yaml
- chat-pvcs.yaml
- model-gate-rbac.yaml - model-gate-rbac.yaml
- ariadne-handoff-rbac.yaml - ariadne-handoff-rbac.yaml
- model-gate-state.yaml - model-gate-state.yaml

View File

@ -45,7 +45,7 @@ spec:
matchLabels: matchLabels:
app: oauth2-proxy-hermes-triage app: oauth2-proxy-hermes-triage
ports: ports:
- {protocol: TCP, port: 9119} - {protocol: TCP, port: 8787}
- from: - from:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
@ -72,7 +72,7 @@ spec:
matchLabels: matchLabels:
app: oauth2-proxy-hermes-agent app: oauth2-proxy-hermes-agent
ports: ports:
- {protocol: TCP, port: 9119} - {protocol: TCP, port: 8787}
egress: egress:
- to: - to:
- namespaceSelector: - namespaceSelector:
@ -310,5 +310,5 @@ spec:
operator: In operator: In
values: [hermes, hermes-agent, hermes-chat-router] values: [hermes, hermes-agent, hermes-chat-router]
ports: ports:
- {protocol: TCP, port: 9119} - {protocol: TCP, port: 8787}
- {protocol: TCP, port: 8080} - {protocol: TCP, port: 8080}

View File

@ -96,7 +96,7 @@ spec:
- --cookie-samesite=lax - --cookie-samesite=lax
- --cookie-refresh=1h - --cookie-refresh=1h
- --cookie-expire=8h - --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 - --http-address=0.0.0.0:4180
- --skip-provider-button=true - --skip-provider-button=true
- --reverse-proxy=true - --reverse-proxy=true
@ -186,7 +186,7 @@ spec:
- --cookie-samesite=lax - --cookie-samesite=lax
- --cookie-refresh=1h - --cookie-refresh=1h
- --cookie-expire=8h - --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 - --http-address=0.0.0.0:4180
- --skip-provider-button=true - --skip-provider-button=true
- --reverse-proxy=true - --reverse-proxy=true

View File

@ -50,9 +50,9 @@ var deniedPrefixes = []string{
"/api/cron", "/api/curator", "/api/dashboard/config", "/api/env", "/api/cron", "/api/curator", "/api/dashboard/config", "/api/env",
"/api/escape", "/api/extensions", "/api/file", "/api/folder", "/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/gateway", "/api/git", "/api/git-info", "/api/health/restart", "/api/kanban", "/api/logs", "/api/mcp",
"/api/memory", "/api/messaging", "/api/notes", "/api/ops", "/api/messaging", "/api/notes", "/api/ops",
"/api/oauth", "/api/onboarding/oauth", "/api/pairing", "/api/plugins", "/api/profiles", "/api/providers", "/api/oauth", "/api/onboarding/oauth", "/api/pairing", "/api/plugins", "/api/providers",
"/api/rollback", "/api/shutdown", "/api/skills", "/api/terminal", "/api/rollback", "/api/shutdown", "/api/terminal",
"/api/tools", "/api/updates", "/api/webhooks", "/api/wiki", "/api/tools", "/api/updates", "/api/webhooks", "/api/wiki",
"/api/workspace", "/api/workspaces", "/api/workspace", "/api/workspaces",
} }

View File

@ -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) { func TestWebUIModelAndReasoningOverridesAreProxied(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost { if request.Method != http.MethodPost {

View File

@ -36,7 +36,7 @@ spec:
port: 8642 port: 8642
targetPort: api targetPort: api
- name: dashboard - name: dashboard
port: 9119 port: 8787
targetPort: dashboard targetPort: dashboard
--- ---
apiVersion: v1 apiVersion: v1
@ -52,7 +52,7 @@ spec:
app: hermes-agent app: hermes-agent
ports: ports:
- name: dashboard - name: dashboard
port: 9119 port: 8787
targetPort: dashboard targetPort: dashboard
--- ---
apiVersion: v1 apiVersion: v1