Skip to content

Commit 7191c72

Browse files
committed
feat: Clawdi platform integration patches
Four minimal patches for Clawdi's multi-tenant dashboard integration: 1. web_server.py: HERMES_SESSION_TOKEN env override — reverse-proxied deployments set a stable token so the SPA can authenticate across restarts (mirrors upstream PR NousResearch#9800 pattern). 2. web_server.py: load_hermes_dotenv() at module init — uvicorn launches web_server.py directly, bypassing hermes_cli/main.py where the dotenv loader normally runs. Without this, the HERMES_SESSION_TOKEN written to ~/.hermes/.env is never loaded into os.environ. 3. web_server.py: expose plain `value` field for non-password env vars in GET /api/env — lets the dashboard round-trip comma-separated allow-lists (TELEGRAM_ALLOWED_USERS, FEISHU_ALLOWED_USERS, etc.) without forcing users to retype them. 4. config.py: register FEISHU_ALLOWED_USERS in OPTIONAL_ENV_VARS — the Feishu gateway adapter reads this from os.getenv() but it was missing from the registration list, so GET /api/env never surfaced it.
1 parent 524cbab commit 7191c72

2 files changed

Lines changed: 53 additions & 5 deletions

File tree

hermes_cli/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,6 +2349,21 @@ def _ensure_hermes_home_managed(home: Path):
23492349
"category": "messaging",
23502350
"advanced": True,
23512351
},
2352+
# Feishu credentials (FEISHU_APP_ID / FEISHU_APP_SECRET / etc.) live
2353+
# in `_EXTRA_ENV_KEYS` because the canonical Feishu config path is
2354+
# `config.platforms.feishu.extra` (not env vars). The allow-list is
2355+
# the exception: `gateway/platforms/feishu.py` reads it ONLY from
2356+
# `os.getenv("FEISHU_ALLOWED_USERS")` (no `extra.allowed_users`
2357+
# fallback), so users have no way to set it without env support.
2358+
# Surface it here so the dashboard can display and round-trip the
2359+
# current list like the other platforms' allowed-users entries.
2360+
"FEISHU_ALLOWED_USERS": {
2361+
"description": "Comma-separated Feishu open IDs allowed to use the bot",
2362+
"prompt": "Allowed Feishu open IDs (comma-separated)",
2363+
"url": "https://open.feishu.cn",
2364+
"password": False,
2365+
"category": "messaging",
2366+
},
23522367
"GATEWAY_ALLOW_ALL_USERS": {
23532368
"description": "Allow all users to interact with messaging bots (true/false). Default: false.",
23542369
"prompt": "Allow all users (true/false)",

hermes_cli/web_server.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,23 @@
4747
check_config_version,
4848
redact_key,
4949
)
50+
from hermes_cli.env_loader import load_hermes_dotenv
5051
from gateway.status import get_running_pid, read_runtime_status
5152

53+
# Load `~/.hermes/.env` into ``os.environ`` before any module-level reads
54+
# of ``HERMES_SESSION_TOKEN`` (or any other secret used by this module).
55+
#
56+
# The `hermes` CLI entry point already does this in ``hermes_cli/main.py``,
57+
# but the standalone uvicorn launcher (``uvicorn hermes_cli.web_server:app``)
58+
# bypasses ``main.py`` and so never gets a chance to load the dotenv file.
59+
# Without this call, reverse-proxied deployments that store
60+
# ``HERMES_SESSION_TOKEN`` in ``~/.hermes/.env`` would silently fall back
61+
# to the random token and break cross-origin auth on every restart.
62+
#
63+
# The ``project_env`` argument mirrors the CLI's call site so development
64+
# checkouts see the same precedence order in both launch paths.
65+
load_hermes_dotenv(project_env=PROJECT_ROOT / ".env")
66+
5267
try:
5368
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
5469
from fastapi.middleware.cors import CORSMiddleware
@@ -67,11 +82,21 @@
6782
app = FastAPI(title="Hermes Agent", version=__version__)
6883

6984
# ---------------------------------------------------------------------------
70-
# Session token for protecting sensitive endpoints (reveal).
71-
# Generated fresh on every server start — dies when the process exits.
72-
# Injected into the SPA HTML so only the legitimate web UI can use it.
85+
# Session token for protecting sensitive /api/* endpoints.
86+
#
87+
# By default, generated fresh on every server start and dies with the
88+
# process — injected into the SPA HTML so the legitimate same-origin
89+
# web UI picks it up without any external coordination. This is the
90+
# single-user ``hermes web`` flow: one process, one SPA, same token.
91+
#
92+
# For reverse-proxied deployments where the UI lives on a different
93+
# origin (e.g. a platform dashboard talking to many per-pod Hermes
94+
# instances over the network), the downstream caller can't read the
95+
# HTML injection, so it sets ``HERMES_SESSION_TOKEN`` to a stable
96+
# value both sides share. Falls back to the random default when the
97+
# env var is unset, preserving stand-alone CLI behavior.
7398
# ---------------------------------------------------------------------------
74-
_SESSION_TOKEN = secrets.token_urlsafe(32)
99+
_SESSION_TOKEN = os.environ.get("HERMES_SESSION_TOKEN") or secrets.token_urlsafe(32)
75100
_SESSION_HEADER_NAME = "X-Hermes-Session-Token"
76101

77102
# In-browser Chat tab (/chat, /api/pty, …). Off unless ``hermes dashboard --tui``
@@ -1219,13 +1244,21 @@ async def get_env_vars():
12191244
result = {}
12201245
for var_name, info in OPTIONAL_ENV_VARS.items():
12211246
value = env_on_disk.get(var_name)
1247+
is_password = info.get("password", False)
12221248
result[var_name] = {
12231249
"is_set": bool(value),
12241250
"redacted_value": redact_key(value) if value else None,
1251+
# Plain on-disk value for non-secret fields (allow-lists,
1252+
# mode flags, account IDs). Lets reverse-proxied dashboards
1253+
# round-trip these in form inputs without forcing the user
1254+
# to retype on every edit. Password-flagged fields stay None
1255+
# and must still go through the rate-limited /api/env/reveal
1256+
# endpoint with audit logging.
1257+
"value": value if (value and not is_password) else None,
12251258
"description": info.get("description", ""),
12261259
"url": info.get("url"),
12271260
"category": info.get("category", ""),
1228-
"is_password": info.get("password", False),
1261+
"is_password": is_password,
12291262
"tools": info.get("tools", []),
12301263
"advanced": info.get("advanced", False),
12311264
}

0 commit comments

Comments
 (0)