157 lines
8.0 KiB
Docker
157 lines
8.0 KiB
Docker
# syntax=docker/dockerfile:1
|
|
# dockerfiles/Dockerfile.hermes-webui
|
|
FROM ghcr.io/nesquena/hermes-webui@sha256:a83a3893111dcb250e7aa7aa657d3d6f4570b0e2fd00d9b7569246fc5e7339b2 AS webui
|
|
|
|
FROM registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
|
|
|
USER root
|
|
|
|
# Keep WebUI and Hermes pinned together. The WebUI imports Hermes internals,
|
|
# while the gateway remains the only process that owns an agent conversation.
|
|
COPY --from=webui /apptoo /opt/hermes-webui
|
|
|
|
# The account policy caps user-selected reasoning at xhigh even when a provider
|
|
# advertises a newer, more expensive level.
|
|
RUN /opt/hermes/.venv/bin/python - <<'PY'
|
|
from pathlib import Path
|
|
|
|
config = Path("/opt/hermes-webui/api/config.py")
|
|
source = config.read_text(encoding="utf-8")
|
|
before = 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max")'
|
|
after = 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")'
|
|
if before not in source:
|
|
raise SystemExit("Hermes WebUI reasoning-effort patch context changed")
|
|
config.write_text(source.replace(before, after, 1), encoding="utf-8")
|
|
|
|
index = Path("/opt/hermes-webui/static/index.html")
|
|
source = index.read_text(encoding="utf-8")
|
|
before = ' <div class="reasoning-option" data-effort="max">Max</div>\n'
|
|
if before not in source:
|
|
raise SystemExit("Hermes WebUI xhigh UI patch context changed")
|
|
index.write_text(source.replace(before, "", 1), encoding="utf-8")
|
|
|
|
# oauth2-proxy returns 401 for browser API and health probes when the secure
|
|
# session expires. Re-enter OIDC with the complete return path instead of
|
|
# presenting an endless, inaccurate "connection lost" loop.
|
|
ui = Path("/opt/hermes-webui/static/ui.js")
|
|
source = ui.read_text(encoding="utf-8")
|
|
before = ''' const res=await fetcher(_offlineHealthUrl(),opts);
|
|
return !!(res&&res.ok);
|
|
'''
|
|
after = ''' const res=await fetcher(_offlineHealthUrl(),opts);
|
|
if(res&&(res.status===401||res.status===403)){
|
|
const rd=window.location.pathname+window.location.search+window.location.hash;
|
|
window.location.assign('/oauth2/start?rd='+encodeURIComponent(rd));
|
|
return false;
|
|
}
|
|
return !!(res&&res.ok);
|
|
'''
|
|
if source.count(before) != 1:
|
|
raise SystemExit("Hermes WebUI auth-recovery patch context changed")
|
|
ui.write_text(source.replace(before, after, 1), encoding="utf-8")
|
|
|
|
# Make delegated session hierarchy obvious and collapsible in the sidebar.
|
|
sessions = Path("/opt/hermes-webui/static/sessions.js")
|
|
source = sessions.read_text(encoding="utf-8")
|
|
before = ''' const childLabel=t('session_meta_children', childCount);
|
|
childCountEl.textContent=childLabel;
|
|
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
|
|
'''
|
|
after = ''' const childLabel=t('session_meta_children', childCount);
|
|
const childrenExpanded=_expandedChildSessionKeys.has(lineageKey)||!!searchQueryRaw;
|
|
childCountEl.textContent=(childrenExpanded?'▾ ':'▸ ')+childLabel;
|
|
childCountEl.setAttribute('aria-expanded',childrenExpanded?'true':'false');
|
|
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
|
|
'''
|
|
if source.count(before) != 1:
|
|
raise SystemExit("Hermes WebUI child-session toggle patch context changed")
|
|
sessions.write_text(source.replace(before, after, 1), encoding="utf-8")
|
|
|
|
# A profile's model is only its default; a session-level selector can override
|
|
# it. Label the scope so the dropdown does not contradict the effective model.
|
|
panels = Path("/opt/hermes-webui/static/panels.js")
|
|
source = panels.read_text(encoding="utf-8")
|
|
before = " if (typeof p.model === 'string' && p.model) meta.push(p.model.split('/').pop());\n"
|
|
after = ''' if (typeof p.model === 'string' && p.model) {
|
|
const routeLabels = {
|
|
'atlas/auto/fast': 'Automatic · Fast',
|
|
'atlas/auto/balanced': 'Automatic · Balanced',
|
|
'atlas/auto/deep': 'Automatic · Deep',
|
|
'atlas/auto/maximum': 'Automatic · Maximum',
|
|
};
|
|
meta.push('profile default: ' + (routeLabels[p.model] || p.model.split('/').pop()));
|
|
}
|
|
'''
|
|
if source.count(before) != 2:
|
|
raise SystemExit("Hermes WebUI profile-model label patch context changed")
|
|
panels.write_text(source.replace(before, after, 2), encoding="utf-8")
|
|
PY
|
|
|
|
# Add the Atlas voice bridge as a narrow integration layer. It activates only
|
|
# when a tenant's server-side STT capability reports the private Jetson route.
|
|
COPY dockerfiles/hermes-webui-atlas-patch.py /tmp/hermes-webui-atlas-patch.py
|
|
COPY dockerfiles/hermes-webui-telegram-project-patch.py /tmp/hermes-webui-telegram-project-patch.py
|
|
COPY dockerfiles/hermes-webui-atlas-voice.js /opt/hermes-webui/static/atlas-voice.js
|
|
COPY dockerfiles/hermes-webui-router-patch.py /tmp/hermes-webui-router-patch.py
|
|
COPY dockerfiles/hermes-webui-router.js /opt/hermes-webui/static/atlas-router.js
|
|
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-atlas-patch.py
|
|
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-telegram-project-patch.py
|
|
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-router-patch.py
|
|
|
|
RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \
|
|
&& grep -Fq 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")' \
|
|
/opt/hermes-webui/api/config.py \
|
|
&& ! grep -Fq 'data-effort="max"' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq "window.location.assign('/oauth2/start?rd='" /opt/hermes-webui/static/ui.js \
|
|
&& grep -Fq "childrenExpanded?'▾ ':'▸ '" /opt/hermes-webui/static/sessions.js \
|
|
&& grep -Fq "TELEGRAM_PROJECT_NAME = 'Telegram'" /opt/hermes-webui/api/models.py \
|
|
&& grep -Fq "'atlas/auto/maximum': 'Automatic · Maximum'" /opt/hermes-webui/static/panels.js \
|
|
&& grep -Fq 'Atlas Jetson (private)' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'HERMES_WEBUI_ATLAS_TTS_URL' /opt/hermes-webui/api/routes.py \
|
|
&& grep -Fq "capability.provider!=='local_command'" /opt/hermes-webui/static/atlas-voice.js \
|
|
&& grep -Fq 'data-priority="maximum"' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'routing_priority:priority' /opt/hermes-webui/static/atlas-router.js \
|
|
&& grep -Fq "'atlas/auto/fast':'AUTO · Fast'" /opt/hermes-webui/static/atlas-router.js \
|
|
&& grep -Fq 'explicit_reasoning_effort' /opt/hermes-webui/api/gateway_chat.py \
|
|
&& /opt/hermes/.venv/bin/python -m py_compile \
|
|
/opt/hermes-webui/api/routes.py \
|
|
/opt/hermes-webui/api/gateway_chat.py
|
|
|
|
# Exercise the real server process in the target architecture before publish.
|
|
RUN set -eu; \
|
|
mkdir -p /tmp/hermes-webui-smoke/home /tmp/hermes-webui-smoke/state /tmp/hermes-webui-smoke/workspace; \
|
|
HERMES_HOME=/tmp/hermes-webui-smoke/home \
|
|
HOME=/tmp/hermes-webui-smoke/home \
|
|
HERMES_WEBUI_STATE_DIR=/tmp/hermes-webui-smoke/state \
|
|
HERMES_WEBUI_DEFAULT_WORKSPACE=/tmp/hermes-webui-smoke/workspace \
|
|
HERMES_WEBUI_HOST=127.0.0.1 \
|
|
HERMES_WEBUI_PORT=18787 \
|
|
HERMES_WEBUI_SKIP_ONBOARDING=1 \
|
|
/opt/hermes/.venv/bin/python /opt/hermes-webui/server.py >/tmp/hermes-webui-smoke.log 2>&1 & \
|
|
server_pid=$!; \
|
|
ready=0; \
|
|
for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do \
|
|
if /opt/hermes/.venv/bin/python -c 'from urllib.request import urlopen; urlopen("http://127.0.0.1:18787/health", timeout=2).read()' >/dev/null 2>&1; then ready=1; break; fi; \
|
|
sleep 1; \
|
|
done; \
|
|
kill "${server_pid}" 2>/dev/null || true; \
|
|
wait "${server_pid}" 2>/dev/null || true; \
|
|
if [ "${ready}" != "1" ]; then cat /tmp/hermes-webui-smoke.log; exit 1; fi; \
|
|
rm -rf /tmp/hermes-webui-smoke /tmp/hermes-webui-smoke.log
|
|
|
|
ENV HERMES_WEBUI_AGENT_DIR=/opt/hermes \
|
|
HERMES_WEBUI_HOST=0.0.0.0 \
|
|
HERMES_WEBUI_PORT=8787 \
|
|
HERMES_WEBUI_CHAT_BACKEND=gateway \
|
|
HERMES_WEBUI_GATEWAY_BASE_URL=http://127.0.0.1:8642 \
|
|
HERMES_WEBUI_GATEWAY_USE_RUNS_API=true \
|
|
HERMES_WEBUI_SKIP_ONBOARDING=1 \
|
|
HERMES_WEBUI_SECURE=1 \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
PYTHONUNBUFFERED=1
|
|
|
|
WORKDIR /opt/hermes-webui
|
|
USER 10000:10000
|
|
EXPOSE 8787
|
|
ENTRYPOINT ["/opt/hermes/.venv/bin/python", "/opt/hermes-webui/server.py"]
|