# syntax=docker/dockerfile:1
# dockerfiles/Dockerfile.hermes-agent
FROM nousresearch/hermes-agent@sha256:9c841866021c54c4596849f6135717e8a4d52ba510b7f52c50aef1de1a283973

USER root

# Keep dashboard chat sockets tied to the intended React mount and conversation.
# A resumed conversation needs a different PTY attachment key from a fresh chat;
# reconnects to that same conversation must keep using the same key.
RUN node <<'NODE'
const fs = require("node:fs");
const path = "/opt/hermes/web/src/pages/ChatPage.tsx";
let source = fs.readFileSync(path, "utf8");
const socketBefore = [
  '      const url = await api.buildWsUrl("/api/pty", params);',
  '      const ws = new WebSocket(url);',
].join("\n");
const socketAfter = [
  '      const url = await api.buildWsUrl("/api/pty", params);',
  '      if (unmounting) return;',
  '      const ws = new WebSocket(url);',
].join("\n");
const attachBefore = '      params.attach = ptyAttachToken(forceFresh);';
const attachAfter = [
  '      const attachScope = resumeParam',
  '        ? `resume:${resumeParam}:${scopedProfile ?? ""}`',
  '        : `fresh:${scopedProfile ?? ""}`;',
  '      params.attach = `${ptyAttachToken(forceFresh)}:${attachScope}`;',
].join("\n");

if (!source.includes(socketBefore)) {
  throw new Error("Hermes ChatPage WebSocket patch context changed");
}
if (!source.includes(attachBefore)) {
  throw new Error("Hermes ChatPage PTY attachment patch context changed");
}
source = source.replace(socketBefore, socketAfter);
source = source.replace(attachBefore, attachAfter);
fs.writeFileSync(path, source);
NODE

# The upstream OIDC gate authenticates users but deliberately treats the
# dashboard as one shared workstation. Allow a deployment to narrow that
# workstation to explicit OIDC subjects. Enforce this after normal provider
# verification so a denied account is a 403, not a misleading provider 503.
RUN python - <<'PY'
from pathlib import Path

path = Path("/opt/hermes/hermes_cli/dashboard_auth/middleware.py")
source = path.read_text()
helper_before = '''def _client_ip(request: Request) -> str:
    fwd = request.headers.get("x-forwarded-for", "")
    if fwd:
        return fwd.split(",")[0].strip()
    return request.client.host if request.client else ""


'''
helper_after = helper_before + '''def _dashboard_user_allowed(session) -> bool:
    """Apply an optional deployment-level OIDC-subject allowlist."""
    import os

    allowed = {
        value.strip()
        for value in os.environ.get(
            "HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS", ""
        ).split(",")
        if value.strip()
    }
    return not allowed or session.user_id in allowed


def _user_forbidden_response() -> Response:
    """Return an authorization failure without exposing identities."""
    return JSONResponse(
        {
            "error": "forbidden",
            "detail": "This Atlas account is not authorized for this dashboard.",
        },
        status_code=403,
    )


'''
refresh_before = '''            new_session, refreshing_provider = refreshed
            request.state.session = new_session
            response = await call_next(request)
'''
refresh_after = '''            new_session, refreshing_provider = refreshed
            if not _dashboard_user_allowed(new_session):
                return _user_forbidden_response()
            request.state.session = new_session
            response = await call_next(request)
'''
final_before = '''    request.state.session = session
    return await call_next(request)
'''
final_after = '''    if not _dashboard_user_allowed(session):
        return _user_forbidden_response()
    request.state.session = session
    return await call_next(request)
'''
for before, after, label in (
    (helper_before, helper_after, "allowlist helper"),
    (refresh_before, refresh_after, "refreshed session"),
    (final_before, final_after, "verified session"),
):
    if before not in source:
        raise SystemExit(f"Hermes dashboard auth {label} patch context changed")
    source = source.replace(before, after, 1)
path.write_text(source)
PY

COPY dockerfiles/hermes-session-migrate.py /opt/hermes/bin/hermes-session-migrate

RUN cd /opt/hermes/web \
    && npm run build \
    && grep -Fq 'if (unmounting) return;' src/pages/ChatPage.tsx \
    && grep -Fq 'resume:${resumeParam}' src/pages/ChatPage.tsx \
    && grep -Fq 'HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS' \
        /opt/hermes/hermes_cli/dashboard_auth/middleware.py \
    && chmod 0755 /opt/hermes/bin/hermes-session-migrate
