fix(hermes): expose private files and reconnect Herdr
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 171
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 171
This commit is contained in:
parent
89243ccef8
commit
9f2e05a936
@ -389,7 +389,14 @@ spec:
|
|||||||
--terminal-type xterm-256color \
|
--terminal-type xterm-256color \
|
||||||
--client-option "titleFixed=Hermes Agent - HERDR" \
|
--client-option "titleFixed=Hermes Agent - HERDR" \
|
||||||
--client-option fontSize=15 \
|
--client-option fontSize=15 \
|
||||||
|
/bin/sh -c '
|
||||||
|
while true; do
|
||||||
/opt/data/tools/bin/herdr
|
/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:
|
ports:
|
||||||
- {name: herdr-tui, containerPort: 7681, protocol: TCP}
|
- {name: herdr-tui, containerPort: 7681, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -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: "20260809-telegram-link-feedback"
|
ai.bstein.dev/config-rev: "20260809-private-workspace-files"
|
||||||
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:5ddffcd02d5debf5566e3ece19f0a348c554b541e21a95cad0fcd85cab6941a3
|
image: registry.bstein.dev/bstein/hermes-chat-router@sha256:165dd52fd1ff6da9f857517e9394d0c61a888311a88ba4be0aaea89315073d90
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
ports:
|
ports:
|
||||||
- {name: http, containerPort: 8080, protocol: TCP}
|
- {name: http, containerPort: 8080, protocol: TCP}
|
||||||
|
|||||||
@ -247,7 +247,26 @@ func authenticatedSubject(request *http.Request) string {
|
|||||||
return ""
|
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 {
|
for _, prefix := range deniedPrefixes {
|
||||||
if path == prefix || strings.HasPrefix(path, prefix+"/") {
|
if path == prefix || strings.HasPrefix(path, prefix+"/") {
|
||||||
return true
|
return true
|
||||||
@ -278,7 +297,7 @@ func (router *tenantRouter) ServeHTTP(writer http.ResponseWriter, request *http.
|
|||||||
if router.serveTelegramWeb(writer, request, subject) {
|
if router.serveTelegramWeb(writer, request, subject) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pathDenied(request.URL.Path) {
|
if pathDenied(request.Method, request.URL.Path) {
|
||||||
http.Error(writer, "chat administration is disabled", http.StatusForbidden)
|
http.Error(writer, "chat administration is disabled", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
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 {
|
||||||
|
|||||||
@ -209,6 +209,11 @@ def test_agent_installs_hermes_integration_before_startup():
|
|||||||
assert "--kind hermes" in server_command
|
assert "--kind hermes" in server_command
|
||||||
assert "--timeout 60000" 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():
|
def test_agent_mounts_auto_router_into_both_hermes_runtimes():
|
||||||
deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
|
deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user