- Add prominent 'Read this before you deploy' section warning that HF
accounts are being suspended for always-on free-tier agent Spaces;
the Cloudflare keep-alive worker is the likely trigger. Recommend a
throwaway account or paid tier, and keeping off-platform backups.
- Update 'What You Get' to include /hm/logs, /hm/desktop-app-setup, /hmd.
- Add Hermes desktop app section (stable persisted session token,
/hm/desktop-app-setup, local-vs-remote feature split).
- Note OAuth + ~/.hermes persistence and config.yaml redaction in the
data-safety section, with the caveat that a suspended account loses
the backup dataset too.
- Add Ollama prefix + full provider-prefix list to quick start, plus an
Ollama subsection.
- Add debugging-without-SSH endpoints table (/hm/logs,
/hm/debug/model-options[-trace], /status).
- Update persistence details: atomic restore, hot-path pruning,
Tini + SIGTERM, file lock.
- Update architecture diagram/prose: Tini as PID 1, wait -n watchdog,
/h…
Reverts commits b0371e0, 060f85e, eecc3ba — the Android app
compatibility changes that broke the WebUI login loop and didn't
fix the Android app anyway.
Removed:
- /api/sessions gateway route (conflicted with WebUI's own /api/sessions)
- Origin header strip for gateway proxy (was for gateway CORS 403)
health-server.js is back to the state from bf86338 (db-wal fix).
fix(webui): only route /api/sessions to gateway for Bearer-auth requests
Farhan Beg
eecc3ba
The previous /api/sessions route (commit b0371e0) intercepted ALL
/api/sessions requests, including the WebUI's own session-list calls.
The WebUI (nesquena/hermes-webui) calls GET /api/sessions with cookie
auth to list sessions. My route sent those to the gateway (port 8642)
instead, which returned a different JSON format → the WebUI's JS
didn't recognize the sessions → login loop (POST /api/auth/login → 200,
GET /session/{id} → 200, GET /login?next=... → 200, repeat).
Fix: only route /api/sessions to the gateway when the request carries a
Bearer token (Authorization: Bearer ...). The Android app and other API
clients send Bearer → go to the gateway. The WebUI uses cookies (no
Bearer) → falls through to the WebUI catch-all as before.
fix(gateway): strip Origin header for gateway proxy — CORS 403
Farhan Beg
060f85e
The gateway's CORS middleware (api_server.py:557-558) returns 403 for
ANY non-empty Origin header when API_SERVER_CORS_ORIGINS is not
configured. The router was forwarding Origin: http://127.0.0.1:8642 on
all proxied requests (needed for the dashboard's own origin check), but
this caused the gateway to reject every /v1/* and /api/sessions request
with 403 Forbidden — even with a valid Bearer token.
Fix: strip the Origin header (set to empty string) when proxying to the
gateway (port 8642). The gateway treats empty Origin as a non-browser
client and allows it (api_server.py:886-887). The dashboard (port 9119)
still gets the local Origin rewrite it needs.
This fixes the Hermes Android app (and any other API client) getting
'invalid api key' / 403 when connecting through the Space.
feat: route /api/sessions to gateway for Android app compat
Farhan Beg
b0371e0
The Hermes Android app (rusty4444/hermes-android) calls the gateway's
session API at /api/sessions and /api/sessions/{id}/messages directly
(not through /v1/). Without this route, those requests hit the WebUI
catch-all, which uses a different auth scheme (cookie/session, not
Bearer) → 401 'invalid api key', even when the correct GATEWAY_TOKEN
is provided.
Fix: route /api/sessions and /api/sessions/* to the gateway (port 8642)
with the same Bearer auth as /v1/*. The WebUI's own /api/* calls use
cookie auth and don't hit /api/sessions, so there's no conflict.
This also benefits any other OpenAI-compatible client that calls the
gateway's /api/sessions endpoint directly instead of through /v1/.
fix(restore): exempt .db files from size cap — state.db was 52MB > 50MB limit
Farhan Beg
bf86338
The real root cause of session loss: state.db (the SQLite database
holding all session data) is 52 MB, which exceeds the 50 MB
MAX_FILE_SIZE_BYTES cap. should_exclude() returned True for state.db →
it was silently skipped on BOTH backup and restore.
The 50 MB cap was meant for model caches and other large non-essential
artifacts, not for essential SQLite databases. As the user's session
history grew past 50 MB, state.db started being excluded — sessions
appeared in the sessions/*.json transcripts (which are small files,
backed up fine) but the main index database was dropped.
Fix:
- Bump MAX_FILE_SIZE_BYTES default from 50 MB to 200 MB (configurable
via SYNC_MAX_FILE_BYTES env var).
- Exempt .db files from the size cap entirely — they're always
essential regardless of size. The cap is for caches, not databases.
This restores state.db (and any other .db file like kanban.db,
response_store.db, lcm.db) to the backup/restore cycle.
chore: trigger HF Space rebuild (db-wal fix not yet built)
Farhan Beg
8b639d8
fix(restore): stop excluding state.db-wal and state.db-shm from backup
Farhan Beg
601366f
SQLite WAL mode stores uncommitted/recent session data in state.db-wal
and the shared-memory index in state.db-shm. The main state.db file only
holds checkpointed (committed) data. By excluding .db-wal and .db-shm
from the HF Dataset backup (inherited from the original HuggingMes
EXCLUDED_SUFFIXES list), recent sessions that hadn't been checkpointed
into state.db were silently dropped from the backup — the user lost
sessions created in the last 13 days.
Fix: only exclude .db-journal (SQLite rollback-journal mode, which is
transient and safe to drop). Keep .db-wal and .db-shm so the full
session state is backed up and restored.
This was a pre-existing bug in the vendored HuggingMes exclusion list,
not introduced by the audit — but it's the root cause of the session
loss reported after the rebuilds.
fix(desktop-app): derive session token from GATEWAY_TOKEN (deterministic)
Farhan Beg
95566a3
The previous approach (generate random + persist to file) had a race:
if a rebuild happened before the sync loop uploaded the token file to
the HF Dataset, the restore on the next boot wouldn't find it → a new
random token was generated → the desktop app needed reconfiguration.
Fix: derive the dashboard session token from API_SERVER_KEY (which comes
from GATEWAY_TOKEN, an HF Space Secret that's stable across restarts)
using HMAC-SHA256. The token is now DETERMINISTIC — same GATEWAY_TOKEN
→ same dashboard token on every boot, with zero dependency on the HF
Dataset backup/restore cycle.
Precedence:
1. HERMES_DASHBOARD_SESSION_TOKEN HF Space Secret (power user override)
2. HMAC-SHA256(API_SERVER_KEY, 'huggingmes-dashboard-session-v1')
— deterministic, recomputed from scratch on every boot
3. Random + persisted to file (fallback when GATEWAY_TOKEN is also
ephemeral — rare)
This eliminates the timing race entirely. The token file is still written
as a fallback for the no-…
fix(desktop-app): allow Electron file:// + localhost:port WS origins
Farhan Beg
d6ec4a6
The CSWSH Origin validation (added in audit Group A, commit 8c51488)
rejected WebSocket upgrades from the Hermes desktop app on 'save and
reconnect' (but not on 'test remote' — the test probe doesn't send an
Origin header).
Root cause: Electron's Chromium engine sends an Origin header on WS
connections. Two cases were rejected:
1. file:// origins — Electron's internal pages use file://, and
new URL('file://...').host is empty → return false → 403.
2. http://localhost:<port> origins — the Electron dev server uses a
random localhost port. new URL(...).host is 'localhost:5173'
(includes port), but the allowlist only had 'localhost' (no port)
→ no match → 403.
Fix:
- Allow file:// origins unconditionally (Electron is a trusted client).
- Match on hostname (no port) in addition to host (with port).
- Explicitly allow any localhost:<port> / 127.0.0.1:<port> origin.
This restores the desktop app's 'save and reconnect' flow while keeping
the CSWSH defense against actual cross-s…
Ollama prefix:
- LLM_MODEL=ollama/llama3 now works out of the box. Ollama is
OpenAI-compatible, so the prefix sets CUSTOM_BASE_URL to
http://127.0.0.1:11434/v1 (overridable via Space Secret for a remote
Ollama instance) and strips the ollama/ prefix from the model name.
- Ollama typically needs no API key; if LLM_API_KEY is set it's passed
through as OPENAI_API_KEY (Ollama ignores it).
- Note: Ollama is not bundled in the HF Space container. For a remote
Ollama server, set CUSTOM_BASE_URL=http://your-ollama:11434/v1.
WRITE_SECRETS_TO_ENV (opt-in):
- Space Secrets are durable (survive restarts) but only live in the
process environment — they're invisible in the dashboard's Env tab,
which reads $HERMES_HOME/.env.
- Set WRITE_SECRETS_TO_ENV=1 as a Space Variable to materialize known
provider keys from the environment into .env on every boot, so the
dashboard shows them. Only writes keys that aren't already in .env
(doesn't clobber user-set values).
- .env is NOT backe…
feat: persist OAuth tokens + all ~/.hermes state across restarts
Farhan Beg
b3b0159
Hermes stores OAuth tokens and credentials under ~/.hermes/ — e.g.
.anthropic_oauth.json, .xai_oauth.json, auth.json (credential pool),
pairing state, plugins. The ~/.hermes directory is on the HF Space's
ephemeral filesystem — NOT under HERMES_HOME (/opt/data, which IS backed
up to the HF Dataset). Previously only ~/.hermes/plugins was symlinked
into the volume, so OAuth logins done via the dashboard were lost on
every container restart.
Fix: replace the plugins-only symlink with a whole-~/.hermes directory
symlink into $HERMES_HOME/home/.hermes. Now ALL auth state (OAuth tokens,
credential pool, pairing, plugins, skills config) lands in the backed-up
tree and survives restarts via hermes-sync.py.
- Runs BEFORE the HF Dataset restore so restored auth files land in the
right place.
- Migrates any pre-existing ~/.hermes contents (from the base image) into
the volume safely before symlinking.
- Removed plugins/ from the top-level HERMES_HOME mkdir (it now lives
under home/.hermes…
feat: stable dashboard session token for desktop app persistence
Farhan Beg
dc9f435
The Hermes desktop app connects via /hmd using the dashboard's session
token. Hermes v0.17.0 generates a random token per process start
(web_server.py:216):
_SESSION_TOKEN = os.environ.get('HERMES_DASHBOARD_SESSION_TOKEN') or secrets.token_urlsafe(32)
Without a stable value, the token changed on every Space restart, forcing
the user to re-scrape it from /hmd/ HTML and reconfigure the desktop app
each time.
start.sh:
- If HERMES_DASHBOARD_SESSION_TOKEN is set as an HF Space Secret, use it
(power users can pin an explicit value).
- Otherwise generate once and persist to
$HERMES_HOME/.huggingmes-dashboard-session-token (backed up to the HF
Dataset via hermes-sync.py — it's not in EXCLUDED_TOP_LEVEL).
- On every boot, read the saved token and export it. The dashboard then
uses this stable value instead of generating a fresh random one.
health-server.js:
- Add dashboardSessionToken to the statusPayload (auth-gated via
/hm/status) so the token is visible without scraping HTML.…
fix(regression): back up config.yaml again, redact api_key on upload
Farhan Beg
da1c88e
Session 2's audit (commit 8c51488) excluded config.yaml from the HF
Dataset backup entirely to prevent plaintext api_key leakage. That was
too aggressive: it also wiped the model/provider/base_url/terminal/
telegram settings on every restart, so dashboard-configured model +
provider settings were lost — the user reported the Keys section no
longer showed the provider after restart.
Fix: re-include config.yaml in the backup, but strip model.api_key from
the STAGED COPY before upload. The non-secret config (model name,
provider, base_url, context_length, compression, telegram, etc.) survives
restarts; the plaintext key does not. The real key comes from the
process environment (Space Secrets) at runtime — Hermes reads
${OPENAI_API_KEY} etc. from env, not from config.yaml.
This restores the pre-audit behavior (config.yaml backed up → dashboard
settings survive) while keeping the security fix (no plaintext keys in
the dataset). The Telegram webhook secret stays excluded.
hotfix: revert setsid bash -c, use subshells + pkill -P for shutdown
Farhan Beg
c1be1ac
The setsid bash -c '...' approach (from Group B) caused the Space to fail
with 'getcwd: cannot access parent directories' on every boot. Root cause:
bash -c spawns a NEW bash that calls getcwd() on startup to initialize
$PWD. The subshell inherits start.sh's CWD, which the HF Dataset restore
wipes — so the new bash fails before the inner 'cd' can run.
The old subshell form '(cd ... && cmd | tee)' worked because the subshell
is forked from the CURRENT bash (which already has $PWD set and doesn't
re-call getcwd()) — the inner cd changes to an absolute path before any
getcwd() is needed.
Revert all three service launches (dashboard, gateway, WebUI) to the
original subshell syntax. Keep setsid on the node router (single process,
no bash -c, no getcwd issue). Update graceful_shutdown to use pkill -P
(kill children of each PID) instead of kill -- -PGID (which required the
setsid group leaders we removed). Tini reaps any remaining orphans.
This loses the clean process-group kill, but pkill…
hotfix: cd to known-good dir before setsid subshells
Farhan Beg
c8547e8
The export-WEBUI_REPO fix wasn't enough. The HF runtime log showed both:
getcwd: cannot access parent directories: No such file or directory
can't open file '/server.py'
The getcwd error happens inside the setsid'd bash -c subshell ON STARTUP
— bash calls getcwd() to initialize $PWD before running any command.
The subshell inherits start.sh's CWD, which was set to
$HERMES_HOME/workspace at line 68 BUT the HF Dataset restore at line 98
wipes/recreates dirs under $HERMES_HOME, so by the time we reach the
service launches the inherited CWD is stale (its inode was deleted).
The old '(cd ... && cmd)' subshell also inherited the stale CWD, but
it never called getcwd() before the inner cd changed away — bash -c
does (to set $PWD for the new shell).
Fix: cd to a guaranteed-valid directory right before the first setsid
launch, so the parent's CWD is fresh when setsid forks the subshell.
hotfix: export WEBUI_REPO so setsid subshell can expand it
Farhan Beg
192769e
Group B wrapped each service launch in 'setsid bash -c "..."' to make
the service a session/process-group leader (so graceful_shutdown can
kill -- -PGID and take down the whole pipeline, not just the subshell).
The single-quoted bash -c string means variables are expanded by the
INNER bash, which starts with a fresh environment and only sees
exported vars. HERMES_HOME was exported (line 29) so the dashboard and
gateway launches worked. But WEBUI_REPO was a plain local var (line 13,
no export) — so inside the WebUI's setsid subshell:
setsid bash -c '(cd "$WEBUI_REPO" && ...)'
^^^^^^^^^^^^ expands to empty
cd "" failed silently → python ran from / → 'can't open file
/server.py: No such file or directory' → WebUI exited → the new
fatal-bind check (B4) exited 1 → Space in error.
Fix: export WEBUI_REPO. This is the only unexported var the setsid
blocks reference (HERMES_HOME, HERMES_WEBUI_PYTHON, and the env vars
are all already exported).
Root cause introduced by co…
quality: dead code, dedup, constants, docs (audit Group E)
Farhan Beg
d8e8033
Quality fixes from the comprehensive audit. No behavior changes — all
syntactic/cosmetic/traceability improvements.
- E1: delete debug-model-options.py. Confirmed dead — not COPY'd into
the image (Dockerfile only copies start.sh, health-server.js,
hermes-sync.py, cloudflare-*.py), no caller anywhere, replaced by the
inline Python probe in health-server.js (/hm/debug/model-options-trace).
Also hardcoded a python3.12 site-packages path that may not match the
container's Python version.
- E7: remove unused 'import time' in cloudflare-proxy-setup.py.
- E11: re-declare ARG HERMES_AGENT_VERSION after FROM so the ENV line
'HERMES_AGENT_VERSION=${HERMES_AGENT_VERSION}' expands correctly
under the classic builder (pre-FROM ARGs are only visible in FROM).
- E12: LOG_ROTATE_BYTES env var replaces the magic 5242880 literal in
the boot log-rotation block.
- E13: record the resolved WebUI commit (git rev-parse HEAD >
/opt/hermes-webui/WEBUI_COMMIT) so a build can be traced to an e…
bugfix: error handlers, timeouts, global guards (audit Group D)
Farhan Beg
7f18e15
Five bug fixes in health-server.js from the comprehensive audit:
- D1 upstream.on('error'): every piped upstream IncomingMessage
(proxyRequest body + non-body paths, proxyDashboard non-rewrite path)
now has an error listener. An unhandled 'error' on a piped stream
throws and can crash the router — a backend socket reset mid-response
would take down every fronted service.
- D2 headersSent guards: the proxyRequest non-body error handler and
the proxyDashboard ClientRequest + upRes error handlers called
res.writeHead() unconditionally. If the response callback already
fired and is streaming, that throws ERR_HTTP_HEADERS_SENT inside the
error handler → unhandled exception. Now guarded; late errors call
res.destroy() instead.
- D3 upstream timeouts: 30s setTimeout on every proxied request
(proxyRequest body + non-body, proxyDashboard). A hung backend that
accepts the socket but never responds previously held the request +
upstream socket open indefinitely, exhaustin…
perf: probes, keep-alive agent, sync hot path, CF idempotency (audit Group C)
Farhan Beg
6303999
Nine performance fixes from the comprehensive audit:
health-server.js:
- statusPayload: parallelize the 3-4 backend probes with Promise.all
(sequential awaits took up to ~2.4s; now ~0.8s worst case) and memoize
the result for 1.5s. Every /health, /hm, /hm/status, /status call
previously re-probed all backends + did 2 synchronous file reads on
the event loop. Cloudflare keepalive + HF probes hit these frequently.
- httpProbe: replace the bare TCP connect (canConnect) with an HTTP GET
that treats a connection error/timeout/non-response as down. A wedged
backend that opens the socket but never responds passed the old check.
Falls back to a TCP probe if the backend doesn't speak HTTP on /.
- readJsonAsync: switch the sync file reads in statusPayload to
fs.promises.readFile so the event loop isn't blocked.
- /hm/logs: stat the file and reject >50MB with a 413 before reading.
A multi-hundred-MB log read synchronously blocked the event loop for
seconds. Switch to fs.promi…
robustness: crash recovery, signal handling, atomicity (audit Group B)
Farhan Beg
ccb8370
Twelve robustness fixes from the comprehensive audit:
Dockerfile:
- Add tini as PID 1 (ENTRYPOINT ["/usr/bin/tini","--",...]). Bash as PID 1
does not forward SIGTERM to children reliably and doesn't reap zombies
from the piped tee/hermes subprocesses. Tini is transparent — the bash
graceful_shutdown trap still runs the final sync-once before exit.
start.sh:
- Child-process watchdog: replace 'wait $GATEWAY_PID' (which only
caught gateway death — a dashboard/WebUI crash after boot left the
container running with a broken UI while /health stayed green) with
'wait -n' over all supervised PIDs. Any child exiting now triggers a
bounded final sync + exit, so HF restarts the whole Space.
- Kill process groups on shutdown, not 'jobs -p'. Services are pipelines
(cmd | tee) in subshells; killing the subshell PID left hermes bound to
its port. Each service now launches with setsid so kill -- -PGID takes
down the whole group.
- graceful_shutdown stops the sync loop FIRST (SIGT…
security: close auth bypasses + secret leaks (audit Group A)
Farhan Beg
8c51488
Eight security fixes from the comprehensive audit:
health-server.js:
- WS upgrade handler enforced NO auth — a client could open a WebSocket
to /v1/* without a bearer or /hm/app/* without the session cookie,
bypassing the entire auth model for WS. Now mirrors the HTTP auth
decision (isAuthorized) on /v1, /hm, /hm/app. /hmd WS stays open by
design (off-Space desktop workspace) but validates Origin against
the Space's public host / SPACE_HOST / ALLOWED_WS_ORIGINS to close
the CSWSH vector. Origin is now only rewritten to the local backend
for /v1 + /hm/app; /hmd forwards the real Origin to the dashboard.
- sanitizeNext: block backslash-prefixed '/\evil.com' open redirect
(browsers normalize backslash to slash → //evil.com) + restrict to
a safe path charset.
- proxyDashboard: cap request body at 20 MB (mirrors proxyRequest) so
an authed user can't OOM the router via /hm/app/api/*.
- /status + /api/status: gate behind requireAuth. They leaked internal
ports, telegram…
perf(restore): skip excluded files at HF Hub + parallelize downloads
Farhan Beg
3bd24f0
restore() was downloading the entire backup dataset and only filtering
post-copy via should_exclude(). That means HF Hub sent every log, .db-wal,
cache file, and >50MB blob even though we threw them away locally. On a
container cold boot this blocked gateway startup for a long time.
Two changes:
1. Pass our EXCLUDED_DIRS / EXCLUDED_TOP_LEVEL / EXCLUDED_SUFFIXES as
snapshot_download(ignore_patterns=...) so HF Hub never transfers
them. should_exclude() still runs post-download as a backstop for
the >50MB size cap, which ignore_patterns can't express.
2. Bump max_workers from the default 8 to 16 (overridable via
SYNC_RESTORE_WORKERS env var) and etag_timeout from 10 to 30 —
many small state files benefit from more concurrency, and the
default etag timeout trips on slow cold HF Hub responses.
Also time the restore and report elapsed seconds in the restored
status message so boot speed is visible in /hm/status and the logs.
fix(config): strip deprecated TERMINAL_CWD from .env on boot
Farhan Beg
70042cf
Hermes v0.17.0 deprecated TERMINAL_CWD in favor of config.yaml's
terminal.cwd and warns on every gateway start:
'Deprecated .env settings detected: TERMINAL_CWD=/opt/data/workspace'
config.yaml already has terminal.cwd set correctly (line 295), so the
.env entry is pure cruft. It's excluded from HF Dataset backup
(hermes-sync.py EXCLUDED_TOP_LEVEL), but the dashboard Env tab / older
Hermes versions can re-add it at runtime. Strip it on every boot right
after config.yaml is written so the warning stays gone.
fix(gateway): cd to known-good dir before launch to fix getcwd() loop
Farhan Beg
69b7182
After the v0.17.0 rebuild the gateway started failing on boot with:
sh: 0: getcwd() failed: No such file or directory
The HF Dataset restore at the top of start.sh wipes/recreates dirs under
$HERMES_HOME, so start.sh's inherited CWD may have been deleted by the
time we reach the gateway launch. v0.17.0's 'hermes gateway' calls
getcwd() more eagerly than the old 'gateway run' did, so it chokes on
the stale CWD and loops.
The dashboard launch (line 376) already guards against this with an
explicit 'cd $HERMES_HOME/workspace &&'. Mirror that for the gateway,
and mkdir -p the workspace/logs dirs first so the cd never fails even
if the restore hasn't created them yet.
fix(gateway): use 'hermes gateway' foreground mode for v0.17.0+
Farhan Beg
e2ccc49
v0.17.0 (v2026.6.19) reworked gateway supervision:
- 'gateway run' is no longer a documented subcommand
- 'gateway restart' now probes for s6, falls back to a systemd user
service and fails with 'linger is not enabled' in the HF Space
container (no s6, no systemd, no SSH)
The HF Space has no process supervisor, so the only supported path is
foreground mode: 'hermes gateway'. This is what start.sh already does
conceptually (background with &, track PID, wait) — just with the
correct command name.
Refs: https://hermes-agent.nousresearch.com/docs/user-guide/messaging
fix(pty): set dashboard cwd to /workspace for desktop file browser
Farhan Beg
8fd94fc
fix(debug): inline Python probe instead of COPY-dependent script file
Farhan Beg
5438f8c
feat(debug): add Python traceback probe for /api/model/options 500
Farhan Beg
8759429
fix(debug): auto-extract session token for /hm/debug/model-options probe
Farhan Beg
defac91
feat(debug): add /hm/debug/model-options to diagnose 500 error
Farhan Beg
bd1e3d7
Directly probes the dashboard /api/model/options endpoint and
returns the raw status, headers, and body. This lets us see
the actual error without needing HF Pro SSH.
feat(logs): add /hm/logs endpoint to view service logs without HF Pro
Farhan Beg
a2311b5
No SSH/Pro needed to see dashboard, gateway, and webui logs. Auth-
gated (same as /hm status page). Supports ?tail=N to limit lines.
Also includes the Origin rewrite fix from the previous commit.
fix(proxy): rewrite Origin header for all proxied requests to dashboard
Farhan Beg
13f65b2
The dashboard's CORS middleware only allows localhost/127.0.0.1
origins. Requests from the desktop app or browser through /hmd carry
the external Origin (e.g. https://<space>.hf.space) which the
dashboard rejects. Rewrite Origin to match the local backend address
so the dashboard accepts the request.
Applies to both proxyRequest() and proxyDashboard() the WebSocket
handler already had this fix.
fix(ws): rewrite Host and Origin headers in WebSocket upgrade to dashboard
Farhan Beg
422f70e
The desktop app connects via /hmd WebSocket to the dashboard, but the
dashboard rejects connections where Origin/Host don't match its local
bind address. The raw TCP proxy in the upgrade handler was forwarding
the original Host header (e.g. <space>.hf.space) which the dashboard
saw as a cross-origin request.
Now the upgrade handler rewrites:
- Host: -> 127.0.0.1:<targetPort>
- Origin: -> http://127.0.0.1:<targetPort>
This matches how the HTTP proxyRequest function already rewrites Host.
fix(proxy): buffer request bodies to avoid chunked Transfer-Encoding, add HERMES_WEBUI_TRUST_FORWARDED_HOST=1
Farhan Beg
1f29b0d
Two fixes for WebUI behind the Node.js reverse proxy:
1. HTTP 501 Unsupported method error: Python BaseHTTPRequestHandler
cannot decode chunked request bodies (Transfer-Encoding: chunked).
Node.js http.request may use chunked encoding when piping req
streams. Fix: buffer the full request body and send it with an
explicit Content-Length header.
2. Cross-origin CSRF mismatch: hermes-webui no longer trusts
X-Forwarded-Host by default. The proxy sets this header, so
browser Origin vs Host checks fail. Fix: set
HERMES_WEBUI_TRUST_FORWARDED_HOST=1 in Dockerfile ENV.
fix(docker): restore proper Dockerfile and add venv chown for hermes user
Farhan Beg
be5c4d4
fix(docker): properly encode Dockerfile and add venv chown for hermes user
Farhan Beg
bc1794a
fix(permissions): chown /opt/hermes/.venv to hermes so auto-updates work
Farhan Beg
2e4f994
Update Dockerfile
Farhan Beg
Unverified signature
0f35c79
docs(sync): tighten Xet flag comment - we don't double-set the legacy var
Farhan Beg
2edd90c
Fix a comment that overstated what the previous patch did. We only set
the new HF_XET_HIGH_PERFORMANCE; older hubs miss the accelerator and
fall back to the regular downloader, which is the right tradeoff for
silencing the deprecation warning.
chore(sync): replace deprecated HF_HUB_ENABLE_HF_TRANSFER with HF_XET_HIGH_PERFORMANCE
Farhan Beg
d434dcb
huggingface_hub 0.30+ deprecated the legacy hf_transfer flag and emits a
FutureWarning at import time when it's set. Switch to the new Xet flag
so the warning stops appearing in Space logs and we actually get the
high-performance transfer on current hub versions.
feat(sync): warn when keys live in ephemeral .env on a Space
Farhan Beg
b047e12
The Hermes dashboard's Env tab writes to $HERMES_HOME/.env which is
intentionally excluded from backup. On HF Spaces the filesystem is
ephemeral, so anything saved there silently disappears on the next
restart and the gateway 500s with 'Provider X is set in config.yaml
but no API key was found'.
* hermes-sync.py: env_warning_payload() detects populated .env on a
Space when SYNC_INCLUDE_ENV is off, surfaces it via write_status() so
every status snapshot carries a structured warning, and prints once
at loop start so it's grep-able in Space logs.
* health-server.js: Backup tile on the HuggingMes status page renders
the warning as a yellow callout when present, and flips the tile
tone to warn so it's visible at a glance.
* README + .env.example: explicit guidance to put provider keys in HF
Space Secrets, with SYNC_INCLUDE_ENV=1 documented as the opt-in
weaker-security alternative. Also fixed stale '10 minute' backup
copy left over from the fixed-interval era.
False-positi…
fix(router): explicit 404 for legacy /api/sessions/<id>/chat/stream
Farhan Beg
ff3dadd
The hermes-workspace capability probe POSTs to that path to detect the
legacy enhanced-fork chat surface. With no rule it fell through to
WebUI, which didn't 404 it cleanly, so the probe set enhancedChat=true,
the workspace then sent chat there at runtime, and the UI showed a
generic 'Authentication error' instead of using /v1/chat/completions.
Returning an explicit 404 makes the workspace's chatMode resolve to
'portable' and route chat through the OpenAI-compat path the Space
actually exposes.
feat(router): add /hmd passthrough for off-Space dashboard access
Farhan Beg
8da84d4
Forwards /hmd/* directly to the internal dashboard (DASHBOARD_PORT)
including /api/*, assets, root HTML and WebSocket upgrades.
Lets remote workspaces (e.g. hermes-workspace) point
HERMES_DASHBOARD_URL=https://<space>/hmd and use the dashboard's own
ephemeral session token without going through HuggingMes auth.
No existing route changes. /hm and /hm/app/* still cookie-gated.
* Replace fixed 600s sleep loop with a 2s stat-only watcher
* Upload 3s after the watched tree settles instead of waiting for the next tick
* Hard ceiling at SYNC_INTERVAL=60s so a constantly-busy session still snapshots
* New tunables: SYNC_POLL_INTERVAL, SYNC_DEBOUNCE_SECONDS
* Reduces worst-case data loss on restart from ~10 minutes to ~3 seconds