diff --git a/dockerfiles/Dockerfile.hermes-agent b/dockerfiles/Dockerfile.hermes-agent index 39fc6c7f8..30f0345c0 100644 --- a/dockerfiles/Dockerfile.hermes-agent +++ b/dockerfiles/Dockerfile.hermes-agent @@ -20,6 +20,74 @@ RUN uv pip install --python /opt/hermes/.venv/bin/python ddgs==9.14.4 # reconnects to that same conversation must keep using the same key. RUN node <<'NODE' const fs = require("node:fs"); +const apiPath = "/opt/hermes/web/src/lib/api.ts"; +let apiSource = fs.readFileSync(apiPath, "utf8"); +const sessionHeaderBefore = [ + 'function setSessionHeader(headers: Headers, token: string): void {', + ' if (!headers.has(SESSION_HEADER)) {', + ' headers.set(SESSION_HEADER, token);', + ' }', + '}', +].join("\n"); +const sessionHeaderAfter = [ + sessionHeaderBefore, + '', + '/**', + ' * Recover an expired outer OAuth2 Proxy session without losing the SPA route.', + ' * Background fetch redirects cannot complete an OIDC browser flow, so API', + ' * routes deliberately return 401 and the dashboard performs this top-level', + ' * navigation once instead.', + ' */', + 'function redirectToProxyLogin(): Promise {', + ' const rd =', + ' window.location.pathname + window.location.search + window.location.hash;', + ' try {', + ' sessionStorage.setItem("hermes.lastLocation", rd);', + ' } catch {', + ' /* privacy mode — the rd query parameter remains authoritative */', + ' }', + ' window.location.assign(', + ' `${BASE}/oauth2/start?rd=${encodeURIComponent(rd)}`,', + ' );', + ' return new Promise(() => {});', + '}', +].join("\n"); +const plainUnauthorizedBefore = [ + ' if (!window.__HERMES_AUTH_REQUIRED__ && !options?.allowUnauthorized) {', +].join("\n"); +const plainUnauthorizedAfter = [ + ' // OAuth2 Proxy API routes intentionally return a plain 401 when its', + ' // session expires. A background fetch cannot complete OIDC, so promote', + ' // it to a top-level navigation and return to this exact dashboard route.', + ' if (window.__HERMES_AUTH_REQUIRED__ && !options?.allowUnauthorized) {', + ' return redirectToProxyLogin();', + ' }', + '', + plainUnauthorizedBefore, +].join("\n"); +const ticketFailureBefore = [ + ' if (!res.ok) {', + ' throw new Error(`/api/auth/ws-ticket: HTTP ${res.status}`);', + ' }', +].join("\n"); +const ticketFailureAfter = [ + ' if (res.status === 401) {', + ' return redirectToProxyLogin<{ ticket: string; ttl_seconds: number }>();', + ' }', + ticketFailureBefore, +].join("\n"); +for (const [before, after, label] of [ + [sessionHeaderBefore, sessionHeaderAfter, "proxy login helper"], + [plainUnauthorizedBefore, plainUnauthorizedAfter, "plain 401 recovery"], + [ticketFailureBefore, ticketFailureAfter, "ticket 401 recovery"], +]) { + if (!apiSource.includes(before)) { + throw new Error(`Hermes API ${label} patch context changed`); + } + apiSource = apiSource.replace(before, after); +} +fs.writeFileSync(apiPath, apiSource); + const path = "/opt/hermes/web/src/pages/ChatPage.tsx"; let source = fs.readFileSync(path, "utf8"); const socketBefore = [ @@ -512,6 +580,8 @@ RUN cd /opt/hermes/web \ && grep -Fq 'resume:${resumeParam}' src/pages/ChatPage.tsx \ && grep -Fq 'eventsRetryAttempt.current' src/components/ChatSidebar.tsx \ && grep -Fq 'reconnecting…' src/components/ChatSidebar.tsx \ + && grep -Fq 'redirectToProxyLogin' src/lib/api.ts \ + && grep -Fq '/oauth2/start?rd=' src/lib/api.ts \ && grep -Fq 'HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS' \ /opt/hermes/hermes_cli/dashboard_auth/middleware.py \ && grep -Fq '_resolve_request_route' \ diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index 63423a2cb..cce85f69f 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -179,7 +179,7 @@ spec: requests: {cpu: 25m, memory: 32Mi} limits: {cpu: 100m, memory: 64Mi} - name: install-agent-tools - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: - sh @@ -227,7 +227,7 @@ spec: requests: {cpu: 100m, memory: 256Mi} limits: {cpu: "1", memory: 1Gi} - name: patch-auth - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -250,7 +250,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 100m, memory: 128Mi} - name: patch-tui-gateway - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -273,7 +273,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 100m, memory: 128Mi} - name: patch-codex-runtime - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -306,7 +306,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 100m, memory: 128Mi} - name: bootstrap-coordinator - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -335,7 +335,7 @@ spec: requests: {cpu: 50m, memory: 128Mi} limits: {cpu: 500m, memory: 512Mi} - name: configure-agent-clients - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: - sh @@ -373,7 +373,7 @@ spec: requests: {cpu: 25m, memory: 32Mi} limits: {cpu: 250m, memory: 128Mi} - name: prepare-ttyd-index - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -395,7 +395,7 @@ spec: limits: {cpu: 250m, memory: 128Mi} containers: - name: hermes - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: [/init, /opt/hermes/docker/main-wrapper.sh] args: [gateway, run] @@ -503,6 +503,7 @@ spec: - --http-address=0.0.0.0:4180 - --skip-provider-button=true - --reverse-proxy=true + - --api-route=^/api/ - --trusted-proxy-ip=10.42.0.0/16 ports: - {name: auth-http, containerPort: 4180, protocol: TCP} @@ -529,7 +530,7 @@ spec: - {name: allowlist, mountPath: /etc/oauth2-proxy, readOnly: true} - {name: oauth-tmp, mountPath: /tmp} - name: terminal - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: [/bin/sh, -ec] args: @@ -622,7 +623,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 500m, memory: 512Mi} - name: cli-lane-runner - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: [/bin/sh, -ec] args: @@ -663,7 +664,7 @@ spec: requests: {cpu: 100m, memory: 256Mi} limits: {cpu: "3", memory: 6Gi} - name: model-steward - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"] env: @@ -689,7 +690,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 250m, memory: 512Mi} - name: image-broker - image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d + image: registry.bstein.dev/bstein/hermes-agent@sha256:222b507ba8d45dd5acb7a33158341fd728b8255b3afae49a74044eaf9029bd1d imagePullPolicy: IfNotPresent command: [/bin/sh, -ec] args: