fix(hermes): expose private files and reconnect Herdr
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 171

This commit is contained in:
jenkins 2026-08-09 09:33:11 -03:00
parent 89243ccef8
commit 9f2e05a936
5 changed files with 71 additions and 5 deletions

View File

@ -389,7 +389,14 @@ spec:
--terminal-type xterm-256color \
--client-option "titleFixed=Hermes Agent - HERDR" \
--client-option fontSize=15 \
/opt/data/tools/bin/herdr
/bin/sh -c '
while true; do
/opt/data/tools/bin/herdr
exit_code=$?
printf "\r\nHerdr disconnected (exit %s); reconnecting in 2 seconds...\r\n" "${exit_code}"
sleep 2
done
'
ports:
- {name: herdr-tui, containerPort: 7681, protocol: TCP}
env:

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: "20260809-telegram-link-feedback"
ai.bstein.dev/config-rev: "20260809-private-workspace-files"
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:5ddffcd02d5debf5566e3ece19f0a348c554b541e21a95cad0fcd85cab6941a3
image: registry.bstein.dev/bstein/hermes-chat-router@sha256:165dd52fd1ff6da9f857517e9394d0c61a888311a88ba4be0aaea89315073d90
imagePullPolicy: IfNotPresent
ports:
- {name: http, containerPort: 8080, protocol: TCP}

View File

@ -247,7 +247,26 @@ func authenticatedSubject(request *http.Request) string {
return ""
}
func pathDenied(path string) bool {
func privateWorkspaceReadAllowed(method, path string) bool {
if method != http.MethodGet {
return false
}
switch path {
case "/api/workspaces", "/api/workspaces/suggest", "/api/file", "/api/file/raw", "/api/folder/download":
return true
default:
return false
}
}
func pathDenied(method, path string) bool {
// Each Keycloak identity is already pinned to a dedicated WebUI and PVC.
// Let that user discover and read files inside the backend-validated private
// Home workspace, while keeping workspace registration/mutation and escape
// APIs behind the chat administration boundary.
if privateWorkspaceReadAllowed(method, path) {
return false
}
for _, prefix := range deniedPrefixes {
if path == prefix || strings.HasPrefix(path, prefix+"/") {
return true
@ -278,7 +297,7 @@ func (router *tenantRouter) ServeHTTP(writer http.ResponseWriter, request *http.
if router.serveTelegramWeb(writer, request, subject) {
return
}
if pathDenied(request.URL.Path) {
if pathDenied(request.Method, request.URL.Path) {
http.Error(writer, "chat administration is disabled", http.StatusForbidden)
return
}

View File

@ -147,6 +147,41 @@ func TestRouterAllowsTenantScopedPersonalization(t *testing.T) {
}
}
func TestRouterAllowsPrivateWorkspaceReadsButBlocksRegistration(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/workspaces",
"/api/workspaces/suggest",
"/api/file",
"/api/file/raw",
"/api/folder/download",
} {
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)
}
}
request := httptest.NewRequest(http.MethodPost, "/api/workspaces/add", strings.NewReader(`{"path":"/opt/data"}`))
request.Header.Set("X-Forwarded-User", "subject")
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("workspace registration got %d, want %d", response.Code, http.StatusForbidden)
}
}
func TestWebUIModelAndReasoningOverridesAreProxied(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost {

View File

@ -209,6 +209,11 @@ def test_agent_installs_hermes_integration_before_startup():
assert "--kind hermes" in server_command
assert "--timeout 60000" in server_command
tui_command = containers["herdr-tui"]["args"][0]
assert "while true" in tui_command
assert "Herdr disconnected" in tui_command
assert "/opt/data/tools/bin/herdr" in tui_command
def test_agent_mounts_auto_router_into_both_hermes_runtimes():
deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())