@bitseek/hermes-webui 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/bin/hermes-webui.mjs +588 -0
- package/package.json +25 -0
- package/scripts/sync-vendor.mjs +74 -0
- package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
- package/templates/systemd/hermes-webui.service +13 -0
- package/templates/windows/hermes-webui-task.ps1 +3 -0
- package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
- package/vendor/agent-frontend-shell/.dockerignore +7 -0
- package/vendor/agent-frontend-shell/.env.docker.example +89 -0
- package/vendor/agent-frontend-shell/.env.example +34 -0
- package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
- package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
- package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
- package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
- package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
- package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
- package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
- package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
- package/vendor/agent-frontend-shell/AGENTS.md +80 -0
- package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
- package/vendor/agent-frontend-shell/BUGS.md +52 -0
- package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
- package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
- package/vendor/agent-frontend-shell/DESIGN.md +173 -0
- package/vendor/agent-frontend-shell/Dockerfile +91 -0
- package/vendor/agent-frontend-shell/LICENSE +21 -0
- package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
- package/vendor/agent-frontend-shell/README.md +705 -0
- package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
- package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
- package/vendor/agent-frontend-shell/TESTING.md +1932 -0
- package/vendor/agent-frontend-shell/THEMES.md +170 -0
- package/vendor/agent-frontend-shell/api/__init__.py +1 -0
- package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
- package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
- package/vendor/agent-frontend-shell/api/auth.py +592 -0
- package/vendor/agent-frontend-shell/api/background.py +87 -0
- package/vendor/agent-frontend-shell/api/clarify.py +238 -0
- package/vendor/agent-frontend-shell/api/commands.py +124 -0
- package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +5178 -0
- package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
- package/vendor/agent-frontend-shell/api/extensions.py +253 -0
- package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
- package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
- package/vendor/agent-frontend-shell/api/goals.py +608 -0
- package/vendor/agent-frontend-shell/api/helpers.py +474 -0
- package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
- package/vendor/agent-frontend-shell/api/metering.py +194 -0
- package/vendor/agent-frontend-shell/api/models.py +4210 -0
- package/vendor/agent-frontend-shell/api/oauth.py +770 -0
- package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
- package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
- package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
- package/vendor/agent-frontend-shell/api/providers.py +2175 -0
- package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
- package/vendor/agent-frontend-shell/api/rollback.py +320 -0
- package/vendor/agent-frontend-shell/api/routes.py +13990 -0
- package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
- package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
- package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
- package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
- package/vendor/agent-frontend-shell/api/session_events.py +45 -0
- package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
- package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
- package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
- package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
- package/vendor/agent-frontend-shell/api/startup.py +128 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
- package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
- package/vendor/agent-frontend-shell/api/system_health.py +167 -0
- package/vendor/agent-frontend-shell/api/terminal.py +410 -0
- package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
- package/vendor/agent-frontend-shell/api/updates.py +1261 -0
- package/vendor/agent-frontend-shell/api/upload.py +322 -0
- package/vendor/agent-frontend-shell/api/usage.py +26 -0
- package/vendor/agent-frontend-shell/api/workspace.py +867 -0
- package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
- package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
- package/vendor/agent-frontend-shell/bootstrap.py +492 -0
- package/vendor/agent-frontend-shell/ctl.sh +427 -0
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
- package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
- package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
- package/vendor/agent-frontend-shell/docker_init.bash +459 -0
- package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
- package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
- package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
- package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
- package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
- package/vendor/agent-frontend-shell/docs/docker.md +337 -0
- package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
- package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
- package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
- package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
- package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
- package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
- package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
- package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
- package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
- package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
- package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
- package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
- package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
- package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
- package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
- package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
- package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
- package/vendor/agent-frontend-shell/extensions/index.js +67 -0
- package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
- package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
- package/vendor/agent-frontend-shell/install.sh +63 -0
- package/vendor/agent-frontend-shell/mcp_server.py +567 -0
- package/vendor/agent-frontend-shell/package.json +12 -0
- package/vendor/agent-frontend-shell/pyproject.toml +56 -0
- package/vendor/agent-frontend-shell/pytest.ini +3 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +624 -0
- package/vendor/agent-frontend-shell/start.ps1 +210 -0
- package/vendor/agent-frontend-shell/start.sh +65 -0
- package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/static/boot.js +1990 -0
- package/vendor/agent-frontend-shell/static/commands.js +1402 -0
- package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
- package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
- package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
- package/vendor/agent-frontend-shell/static/icons.js +92 -0
- package/vendor/agent-frontend-shell/static/index.html +1506 -0
- package/vendor/agent-frontend-shell/static/login.js +177 -0
- package/vendor/agent-frontend-shell/static/manifest.json +53 -0
- package/vendor/agent-frontend-shell/static/messages.js +3521 -0
- package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
- package/vendor/agent-frontend-shell/static/panels.js +7995 -0
- package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
- package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
- package/vendor/agent-frontend-shell/static/style.css +4774 -0
- package/vendor/agent-frontend-shell/static/sw.js +173 -0
- package/vendor/agent-frontend-shell/static/terminal.js +632 -0
- package/vendor/agent-frontend-shell/static/ui.js +8997 -0
- package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
- package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
- package/vendor/agent-frontend-shell/static/workspace.js +680 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hermes Web UI -- HTTP helper functions.
|
|
3
|
+
"""
|
|
4
|
+
import json as _json
|
|
5
|
+
import os
|
|
6
|
+
import re as _re
|
|
7
|
+
import ssl
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from api.config import IMAGE_EXTS, MD_EXTS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Treat stalled/closed HTTP clients as normal disconnects. Long-lived SSE
|
|
13
|
+
# connections often end this way when a browser tab sleeps, a phone switches
|
|
14
|
+
# networks, or Tailscale leaves the socket half-closed.
|
|
15
|
+
_CLIENT_DISCONNECT_ERRORS = (
|
|
16
|
+
BrokenPipeError,
|
|
17
|
+
ConnectionResetError,
|
|
18
|
+
ConnectionAbortedError,
|
|
19
|
+
TimeoutError,
|
|
20
|
+
ssl.SSLError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def require(body: dict, *fields) -> None:
|
|
25
|
+
"""Phase D: Validate required fields. Raises ValueError with clean message."""
|
|
26
|
+
missing = [f for f in fields if not body.get(f) and body.get(f) != 0]
|
|
27
|
+
if missing:
|
|
28
|
+
raise ValueError(f"Missing required field(s): {', '.join(missing)}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bad(handler, msg, status: int=400):
|
|
32
|
+
"""Return a clean JSON error response."""
|
|
33
|
+
return j(handler, {'error': msg}, status=status)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sanitize_error(e: Exception) -> str:
|
|
37
|
+
"""Strip filesystem paths from exception messages before returning to client."""
|
|
38
|
+
import re
|
|
39
|
+
msg = str(e)
|
|
40
|
+
# Remove absolute paths (Unix and Windows)
|
|
41
|
+
msg = re.sub(r'(?:(?:/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))', '<path>', msg)
|
|
42
|
+
return msg
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def safe_resolve(root: Path, requested: str) -> Path:
|
|
46
|
+
"""Resolve a relative path inside root, raising ValueError on traversal."""
|
|
47
|
+
resolved = (root / requested).resolve()
|
|
48
|
+
resolved.relative_to(root.resolve()) # raises ValueError if outside root
|
|
49
|
+
return resolved
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _security_headers(handler):
|
|
53
|
+
"""Add security headers to every response."""
|
|
54
|
+
handler.send_header('X-Content-Type-Options', 'nosniff')
|
|
55
|
+
handler.send_header('X-Frame-Options', 'DENY')
|
|
56
|
+
handler.send_header('Referrer-Policy', 'same-origin')
|
|
57
|
+
handler.send_header(
|
|
58
|
+
'Content-Security-Policy',
|
|
59
|
+
"default-src 'self' https://*.cloudflareaccess.com; "
|
|
60
|
+
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
|
|
61
|
+
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; "
|
|
62
|
+
"img-src 'self' data: https: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cdn.jsdelivr.net; "
|
|
63
|
+
"manifest-src 'self' https://*.cloudflareaccess.com; "
|
|
64
|
+
"base-uri 'self'; form-action 'self'"
|
|
65
|
+
)
|
|
66
|
+
handler.send_header(
|
|
67
|
+
'Permissions-Policy',
|
|
68
|
+
'camera=(), microphone=(self), geolocation=(), clipboard-write=(self)'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _accepts_gzip(handler) -> bool:
|
|
73
|
+
"""Check if the client accepts gzip encoding."""
|
|
74
|
+
headers = getattr(handler, 'headers', None)
|
|
75
|
+
if not headers:
|
|
76
|
+
return False
|
|
77
|
+
ae = headers.get('Accept-Encoding', '')
|
|
78
|
+
return 'gzip' in ae
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _safe_write(handler, body: bytes) -> None:
|
|
82
|
+
"""Write response body, ignoring expected client disconnect errors.
|
|
83
|
+
|
|
84
|
+
Logs disconnects at debug level so they are observable without
|
|
85
|
+
polluting stdout/stderr during normal operation (SSE reconnects,
|
|
86
|
+
tab closes, mobile network switches, etc.).
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
handler.end_headers()
|
|
90
|
+
handler.wfile.write(body)
|
|
91
|
+
except _CLIENT_DISCONNECT_ERRORS as exc:
|
|
92
|
+
import logging
|
|
93
|
+
logging.getLogger("hermes.webui").debug(
|
|
94
|
+
"Client disconnected mid-response (%s): %s",
|
|
95
|
+
type(exc).__name__,
|
|
96
|
+
getattr(handler, "path", "?"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def j(handler, payload, status: int=200, extra_headers: dict=None) -> None:
|
|
101
|
+
"""Send a JSON response.
|
|
102
|
+
|
|
103
|
+
*extra_headers*: optional dict of additional headers to include
|
|
104
|
+
(e.g., {'Set-Cookie': '...'}). Headers are sent before end_headers().
|
|
105
|
+
"""
|
|
106
|
+
body = _json.dumps(payload, ensure_ascii=False, indent=2).encode('utf-8')
|
|
107
|
+
handler.send_response(status)
|
|
108
|
+
handler.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
109
|
+
|
|
110
|
+
# Gzip-compress responses over 1KB when the client accepts it.
|
|
111
|
+
# Typical JSON API responses compress 70-80%, giving a big speedup
|
|
112
|
+
# for large payloads (session history, message lists).
|
|
113
|
+
if _accepts_gzip(handler) and len(body) > 1024:
|
|
114
|
+
import gzip
|
|
115
|
+
body = gzip.compress(body, compresslevel=4)
|
|
116
|
+
handler.send_header('Content-Encoding', 'gzip')
|
|
117
|
+
|
|
118
|
+
handler.send_header('Content-Length', str(len(body)))
|
|
119
|
+
handler.send_header('Cache-Control', 'no-store')
|
|
120
|
+
_security_headers(handler)
|
|
121
|
+
if extra_headers:
|
|
122
|
+
for k, v in extra_headers.items():
|
|
123
|
+
handler.send_header(k, v)
|
|
124
|
+
_safe_write(handler, body)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def t(handler, payload, status: int=200, content_type: str='text/plain; charset=utf-8') -> None:
|
|
128
|
+
"""Send a plain text or HTML response."""
|
|
129
|
+
body = payload if isinstance(payload, bytes) else str(payload).encode('utf-8')
|
|
130
|
+
handler.send_response(status)
|
|
131
|
+
handler.send_header('Content-Type', content_type)
|
|
132
|
+
handler.send_header('Content-Length', str(len(body)))
|
|
133
|
+
handler.send_header('Cache-Control', 'no-store')
|
|
134
|
+
_security_headers(handler)
|
|
135
|
+
_safe_write(handler, body)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
MAX_BODY_BYTES = 20 * 1024 * 1024 # 20MB limit for non-upload POST bodies
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── Credential redaction ──────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
def _build_redact_fn():
|
|
144
|
+
"""Return a redactor backed by hermes-agent plus local fallback patterns."""
|
|
145
|
+
# Fallback mirrors the agent's known credential prefixes so WebUI API
|
|
146
|
+
# responses remain a hard redaction boundary even without hermes-agent.
|
|
147
|
+
# Keep this active even when hermes-agent is importable so API responses do
|
|
148
|
+
# not regress if the agent redactor misses a token shape.
|
|
149
|
+
_CRED_RE = _re.compile(
|
|
150
|
+
r"(?<![A-Za-z0-9_-])("
|
|
151
|
+
r"sk-[A-Za-z0-9_-]{10,}" # OpenAI / Anthropic / OpenRouter
|
|
152
|
+
r"|ghp_[A-Za-z0-9]{10,}" # GitHub PAT (classic)
|
|
153
|
+
r"|github_pat_[A-Za-z0-9_]{10,}" # GitHub PAT (fine-grained)
|
|
154
|
+
r"|gho_[A-Za-z0-9]{10,}" # GitHub OAuth token
|
|
155
|
+
r"|ghu_[A-Za-z0-9]{10,}" # GitHub user-to-server token
|
|
156
|
+
r"|ghs_[A-Za-z0-9]{10,}" # GitHub server-to-server token
|
|
157
|
+
r"|ghr_[A-Za-z0-9]{10,}" # GitHub refresh token
|
|
158
|
+
r"|xox[baprs]-[A-Za-z0-9-]{10,}" # Slack tokens
|
|
159
|
+
r"|AIza[A-Za-z0-9_-]{30,}" # Google API keys
|
|
160
|
+
r"|pplx-[A-Za-z0-9]{10,}" # Perplexity
|
|
161
|
+
r"|fal_[A-Za-z0-9_-]{10,}" # Fal.ai
|
|
162
|
+
r"|fc-[A-Za-z0-9]{10,}" # Firecrawl
|
|
163
|
+
r"|bb_live_[A-Za-z0-9_-]{10,}" # BrowserBase
|
|
164
|
+
r"|gAAAA[A-Za-z0-9_=-]{20,}" # Codex encrypted tokens
|
|
165
|
+
r"|AKIA[A-Z0-9]{16}" # AWS Access Key ID
|
|
166
|
+
r"|sk_live_[A-Za-z0-9]{10,}" # Stripe secret key (live)
|
|
167
|
+
r"|sk_test_[A-Za-z0-9]{10,}" # Stripe secret key (test)
|
|
168
|
+
r"|rk_live_[A-Za-z0-9]{10,}" # Stripe restricted key
|
|
169
|
+
r"|SG\.[A-Za-z0-9_-]{10,}" # SendGrid API key
|
|
170
|
+
r"|hf_[A-Za-z0-9]{10,}" # HuggingFace token
|
|
171
|
+
r"|r8_[A-Za-z0-9]{10,}" # Replicate API token
|
|
172
|
+
r"|npm_[A-Za-z0-9]{10,}" # npm access token
|
|
173
|
+
r"|pypi-[A-Za-z0-9_-]{10,}" # PyPI API token
|
|
174
|
+
r"|dop_v1_[A-Za-z0-9]{10,}" # DigitalOcean PAT
|
|
175
|
+
r"|doo_v1_[A-Za-z0-9]{10,}" # DigitalOcean OAuth
|
|
176
|
+
r"|am_[A-Za-z0-9_-]{10,}" # AgentMail API key
|
|
177
|
+
r"|sk_[A-Za-z0-9_]{10,}" # ElevenLabs TTS key
|
|
178
|
+
r"|tvly-[A-Za-z0-9]{10,}" # Tavily search API key
|
|
179
|
+
r"|exa_[A-Za-z0-9]{10,}" # Exa search API key
|
|
180
|
+
r"|gsk_[A-Za-z0-9]{10,}" # Groq Cloud API key
|
|
181
|
+
r"|syt_[A-Za-z0-9]{10,}" # Matrix access token
|
|
182
|
+
r"|retaindb_[A-Za-z0-9]{10,}" # RetainDB API key
|
|
183
|
+
r"|hsk-[A-Za-z0-9]{10,}" # Hindsight API key
|
|
184
|
+
r"|mem0_[A-Za-z0-9]{10,}" # Mem0 Platform API key
|
|
185
|
+
r"|brv_[A-Za-z0-9]{10,}" # ByteRover API key
|
|
186
|
+
r")(?![A-Za-z0-9_-])"
|
|
187
|
+
)
|
|
188
|
+
_AUTH_HDR_RE = _re.compile(r"(Authorization:\s*Bearer\s+)(\S+)", _re.IGNORECASE)
|
|
189
|
+
_ENV_RE = _re.compile(
|
|
190
|
+
r"([A-Z0-9_]{0,50}(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)[A-Z0-9_]{0,50})"
|
|
191
|
+
r"\s*=\s*(['\"]?)(\S+)\2"
|
|
192
|
+
)
|
|
193
|
+
_PRIVKEY_RE = _re.compile(
|
|
194
|
+
r"-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def _mask(token: str) -> str:
|
|
198
|
+
return f"{token[:6]}...{token[-4:]}" if len(token) >= 18 else "***"
|
|
199
|
+
|
|
200
|
+
def _fallback_redact(text: str) -> str:
|
|
201
|
+
if not isinstance(text, str) or not text:
|
|
202
|
+
return text
|
|
203
|
+
text = _CRED_RE.sub(lambda m: _mask(m.group(1)), text)
|
|
204
|
+
text = _AUTH_HDR_RE.sub(lambda m: m.group(1) + _mask(m.group(2)), text)
|
|
205
|
+
text = _ENV_RE.sub(
|
|
206
|
+
lambda m: f"{m.group(1)}={m.group(2)}{_mask(m.group(3))}{m.group(2)}", text
|
|
207
|
+
)
|
|
208
|
+
text = _PRIVKEY_RE.sub("[REDACTED PRIVATE KEY]", text)
|
|
209
|
+
return text
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
from agent.redact import redact_sensitive_text
|
|
213
|
+
except ImportError:
|
|
214
|
+
return _fallback_redact
|
|
215
|
+
|
|
216
|
+
def _combined_redact(text: str) -> str:
|
|
217
|
+
if not isinstance(text, str) or not text:
|
|
218
|
+
return text
|
|
219
|
+
# WebUI API responses are a hard safety boundary — pass force=True so the
|
|
220
|
+
# agent's broader patterns (Stripe sk_live_, Google AIza…, JWT eyJ…, DB
|
|
221
|
+
# connection strings, Telegram bot tokens) run regardless of the user's
|
|
222
|
+
# HERMES_REDACT_SECRETS opt-in. The local fallback then handles the
|
|
223
|
+
# common short-prefix shapes the agent omits (ghp_, sk-, hf_, AKIA).
|
|
224
|
+
try:
|
|
225
|
+
agent_redacted = redact_sensitive_text(text, force=True)
|
|
226
|
+
except TypeError:
|
|
227
|
+
# Older hermes-agent builds that predate the force kwarg.
|
|
228
|
+
agent_redacted = redact_sensitive_text(text)
|
|
229
|
+
return _fallback_redact(agent_redacted)
|
|
230
|
+
|
|
231
|
+
return _combined_redact
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
_redact_fn_cached = _build_redact_fn()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
_SENSITIVE_CASE_MARKERS = (
|
|
238
|
+
"sk-",
|
|
239
|
+
"ghp_",
|
|
240
|
+
"github_pat_",
|
|
241
|
+
"gho_",
|
|
242
|
+
"ghu_",
|
|
243
|
+
"ghs_",
|
|
244
|
+
"ghr_",
|
|
245
|
+
"AKIA",
|
|
246
|
+
"xoxb-",
|
|
247
|
+
"xoxa-",
|
|
248
|
+
"xoxp-",
|
|
249
|
+
"xoxr-",
|
|
250
|
+
"xoxs-",
|
|
251
|
+
"AIza",
|
|
252
|
+
"pplx-",
|
|
253
|
+
"fal_",
|
|
254
|
+
"fc-",
|
|
255
|
+
"bb_live_",
|
|
256
|
+
"gAAAA",
|
|
257
|
+
"sk_live_",
|
|
258
|
+
"sk_test_",
|
|
259
|
+
"rk_live_",
|
|
260
|
+
"SG.",
|
|
261
|
+
"hf_",
|
|
262
|
+
"r8_",
|
|
263
|
+
"npm_",
|
|
264
|
+
"pypi-",
|
|
265
|
+
"dop_v1_",
|
|
266
|
+
"doo_v1_",
|
|
267
|
+
"am_",
|
|
268
|
+
"sk_",
|
|
269
|
+
"tvly-",
|
|
270
|
+
"exa_",
|
|
271
|
+
"gsk_",
|
|
272
|
+
"syt_",
|
|
273
|
+
"retaindb_",
|
|
274
|
+
"hsk-",
|
|
275
|
+
"mem0_",
|
|
276
|
+
"brv_",
|
|
277
|
+
"eyJ",
|
|
278
|
+
"-----BEGIN",
|
|
279
|
+
)
|
|
280
|
+
_SENSITIVE_LOWER_MARKERS = (
|
|
281
|
+
"authorization: bearer ",
|
|
282
|
+
"private key",
|
|
283
|
+
"postgres://",
|
|
284
|
+
"postgresql://",
|
|
285
|
+
"mysql://",
|
|
286
|
+
"mongodb://",
|
|
287
|
+
"redis://",
|
|
288
|
+
"amqp://",
|
|
289
|
+
"://", # stage-348 Opus SHOULD-FIX: catch http(s)/ws(s)/ftp URL userinfo + sensitive query params (#2171 follow-up)
|
|
290
|
+
"access_token",
|
|
291
|
+
"refresh_token",
|
|
292
|
+
"id_token",
|
|
293
|
+
"api_key",
|
|
294
|
+
"apikey",
|
|
295
|
+
"client_secret",
|
|
296
|
+
"auth_token",
|
|
297
|
+
"raw_secret",
|
|
298
|
+
"secret_input",
|
|
299
|
+
"key_material",
|
|
300
|
+
"x-amz-signature",
|
|
301
|
+
"token=",
|
|
302
|
+
"secret=",
|
|
303
|
+
"password=",
|
|
304
|
+
"authorization=",
|
|
305
|
+
"key=",
|
|
306
|
+
'"token"',
|
|
307
|
+
'"secret"',
|
|
308
|
+
'"password"',
|
|
309
|
+
'"bearer"',
|
|
310
|
+
)
|
|
311
|
+
_SENSITIVE_TELEGRAM_MARKER_RE = _re.compile(r"(?:bot)?\d{8,}:[-A-Za-z0-9_]{30,}")
|
|
312
|
+
_SENSITIVE_DISCORD_MARKER_RE = _re.compile(r"<@!?\d{17,20}>")
|
|
313
|
+
_SENSITIVE_PHONE_MARKER_RE = _re.compile(r"(?<![A-Za-z0-9])\+[1-9]\d{6,14}(?![A-Za-z0-9])")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _might_contain_sensitive_text(text: str) -> bool:
|
|
317
|
+
"""Cheap prefilter before the full agent+fallback redaction pass."""
|
|
318
|
+
if not isinstance(text, str) or not text:
|
|
319
|
+
return False
|
|
320
|
+
if any(marker in text for marker in _SENSITIVE_CASE_MARKERS):
|
|
321
|
+
return True
|
|
322
|
+
lower = text.lower()
|
|
323
|
+
if any(marker in lower for marker in _SENSITIVE_LOWER_MARKERS):
|
|
324
|
+
return True
|
|
325
|
+
if ":" in text and _SENSITIVE_TELEGRAM_MARKER_RE.search(text):
|
|
326
|
+
return True
|
|
327
|
+
if "<@" in text and _SENSITIVE_DISCORD_MARKER_RE.search(text):
|
|
328
|
+
return True
|
|
329
|
+
if "+" in text and _SENSITIVE_PHONE_MARKER_RE.search(text):
|
|
330
|
+
return True
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _redact_text(text: str, *, _enabled: bool | None = None) -> str:
|
|
335
|
+
"""Redact sensitive text from API responses. Respects api_redact_enabled setting.
|
|
336
|
+
|
|
337
|
+
The ``_enabled`` parameter is an internal optimization for callers that
|
|
338
|
+
redact many strings in a single response — `redact_session_data()` reads
|
|
339
|
+
the setting once and threads it through ``_redact_value`` so we avoid
|
|
340
|
+
re-loading settings.json from disk per string. (Opus pre-release perf fix.)
|
|
341
|
+
"""
|
|
342
|
+
if not isinstance(text, str) or not text:
|
|
343
|
+
return text
|
|
344
|
+
if _enabled is None:
|
|
345
|
+
from api.config import load_settings
|
|
346
|
+
_enabled = bool(load_settings().get("api_redact_enabled", True))
|
|
347
|
+
if not _enabled:
|
|
348
|
+
return text
|
|
349
|
+
if not _might_contain_sensitive_text(text):
|
|
350
|
+
return text
|
|
351
|
+
return _redact_fn_cached(text)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _redact_value(v, *, _enabled: bool | None = None):
|
|
355
|
+
"""Recursively redact credentials from strings, dicts, and lists.
|
|
356
|
+
|
|
357
|
+
``_enabled`` is threaded through so a single response-level redact pass
|
|
358
|
+
only reads settings.json once. (Opus pre-release perf fix.)
|
|
359
|
+
"""
|
|
360
|
+
if isinstance(v, str):
|
|
361
|
+
return _redact_text(v, _enabled=_enabled)
|
|
362
|
+
if isinstance(v, dict):
|
|
363
|
+
return {k: _redact_value(val, _enabled=_enabled) for k, val in v.items()}
|
|
364
|
+
if isinstance(v, list):
|
|
365
|
+
return [_redact_value(item, _enabled=_enabled) for item in v]
|
|
366
|
+
return v
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def redact_session_data(session_dict: dict) -> dict:
|
|
370
|
+
"""Redact credentials from message content and tool_call data before API response.
|
|
371
|
+
|
|
372
|
+
Applies to: messages[], tool_calls[], and title.
|
|
373
|
+
The underlying session file is not modified; redaction is response-layer only.
|
|
374
|
+
|
|
375
|
+
Reads the ``api_redact_enabled`` setting ONCE for the entire response and
|
|
376
|
+
threads it through to avoid hundreds of settings.json reads per session
|
|
377
|
+
payload (a 50-message session has hundreds of nested strings). When the
|
|
378
|
+
setting is disabled this is also a fast path: the recursion still walks
|
|
379
|
+
but every string returns early.
|
|
380
|
+
"""
|
|
381
|
+
from api.config import load_settings
|
|
382
|
+
_enabled = bool(load_settings().get("api_redact_enabled", True))
|
|
383
|
+
result = dict(session_dict)
|
|
384
|
+
if isinstance(result.get('title'), str):
|
|
385
|
+
result['title'] = _redact_text(result['title'], _enabled=_enabled)
|
|
386
|
+
if 'messages' in result:
|
|
387
|
+
result['messages'] = _redact_value(result['messages'], _enabled=_enabled)
|
|
388
|
+
if 'tool_calls' in result:
|
|
389
|
+
result['tool_calls'] = _redact_value(result['tool_calls'], _enabled=_enabled)
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def read_body(handler) -> dict:
|
|
394
|
+
"""Read and JSON-parse a POST request body (capped at 20MB)."""
|
|
395
|
+
raw_length = handler.headers.get('Content-Length', 0)
|
|
396
|
+
try:
|
|
397
|
+
length = int(raw_length)
|
|
398
|
+
except (TypeError, ValueError):
|
|
399
|
+
try:
|
|
400
|
+
handler.close_connection = True
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
raise ValueError(f'Invalid Content-Length: {raw_length!r}')
|
|
404
|
+
if length < 0:
|
|
405
|
+
try:
|
|
406
|
+
handler.close_connection = True
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
raise ValueError(f'Invalid Content-Length: {length}')
|
|
410
|
+
if length > MAX_BODY_BYTES:
|
|
411
|
+
try:
|
|
412
|
+
handler.close_connection = True
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
raise ValueError(f'Request body too large ({length} bytes, max {MAX_BODY_BYTES})')
|
|
416
|
+
raw = handler.rfile.read(length) if length else b'{}'
|
|
417
|
+
try:
|
|
418
|
+
return _json.loads(raw)
|
|
419
|
+
except Exception:
|
|
420
|
+
return {}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# ── Profile cookie helpers (issue #798) ─────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
PROFILE_COOKIE_NAME = 'hermes_profile'
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def get_profile_cookie_name() -> str:
|
|
429
|
+
"""Return the cookie name used to persist the active WebUI profile."""
|
|
430
|
+
return os.getenv('WEBUI_PROFILE_COOKIE_NAME', PROFILE_COOKIE_NAME)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def get_profile_cookie(handler) -> str | None:
|
|
434
|
+
"""Extract the active-profile cookie value from the request, or None."""
|
|
435
|
+
cookie_header = handler.headers.get('Cookie', '')
|
|
436
|
+
if not cookie_header:
|
|
437
|
+
return None
|
|
438
|
+
import http.cookies as _hc
|
|
439
|
+
cookie = _hc.SimpleCookie()
|
|
440
|
+
try:
|
|
441
|
+
cookie.load(cookie_header)
|
|
442
|
+
except _hc.CookieError:
|
|
443
|
+
return None
|
|
444
|
+
cookie_name = get_profile_cookie_name()
|
|
445
|
+
morsel = cookie.get(cookie_name)
|
|
446
|
+
if morsel and morsel.value:
|
|
447
|
+
# Validate against profile-name pattern before trusting
|
|
448
|
+
from api.profiles import _PROFILE_ID_RE
|
|
449
|
+
val = morsel.value
|
|
450
|
+
if val == 'default' or _PROFILE_ID_RE.fullmatch(val):
|
|
451
|
+
return val
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def build_profile_cookie(name: str) -> str:
|
|
456
|
+
"""Build a Set-Cookie header value for the active-profile cookie.
|
|
457
|
+
|
|
458
|
+
Always persist the selected profile in the cookie, including 'default'.
|
|
459
|
+
Clearing the cookie causes the backend to fall back to process-global
|
|
460
|
+
_active_profile, which can unexpectedly switch clients back to another
|
|
461
|
+
profile.
|
|
462
|
+
|
|
463
|
+
Set HttpOnly because the UI reads the active profile from
|
|
464
|
+
/api/profile/active JSON and does not need to access this cookie via
|
|
465
|
+
document.cookie.
|
|
466
|
+
"""
|
|
467
|
+
import http.cookies as _hc
|
|
468
|
+
cookie = _hc.SimpleCookie()
|
|
469
|
+
cookie_name = get_profile_cookie_name()
|
|
470
|
+
cookie[cookie_name] = name
|
|
471
|
+
cookie[cookie_name]['path'] = '/'
|
|
472
|
+
cookie[cookie_name]['httponly'] = True
|
|
473
|
+
cookie[cookie_name]['samesite'] = 'Lax'
|
|
474
|
+
return cookie[cookie_name].OutputString()
|