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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",
}

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

View File

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