@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.
Files changed (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. 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()