@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,128 @@
1
+ """Hermes Web UI -- startup helpers."""
2
+ from __future__ import annotations
3
+ import os, stat, subprocess, sys
4
+ from pathlib import Path
5
+
6
+ # Credential files that should never be world-readable
7
+ _SENSITIVE_FILES = (
8
+ '.env',
9
+ 'google_token.json',
10
+ 'google_client_secret.json',
11
+ '.signing_key',
12
+ 'auth.json',
13
+ )
14
+
15
+
16
+ def fix_credential_permissions() -> None:
17
+ """Ensure sensitive files in HERMES_HOME have safe permissions.
18
+
19
+ Respects:
20
+ - HERMES_SKIP_CHMOD=1 → bypass entirely
21
+ - HERMES_HOME_MODE → group bits are allowed if set by the operator,
22
+ only world-readable/world-writable files are fixed
23
+ """
24
+ if os.environ.get('HERMES_SKIP_CHMOD', '').strip() in ('1', 'true'):
25
+ return
26
+
27
+ # Parse operator-declared mode to know if group bits are intentional
28
+ declared_mode = None
29
+ raw_mode = os.environ.get('HERMES_HOME_MODE', '').strip()
30
+ if raw_mode:
31
+ try:
32
+ declared_mode = int(raw_mode, 8)
33
+ except ValueError:
34
+ pass
35
+
36
+ hermes_home = Path(os.environ.get('HERMES_HOME', str(Path.home() / '.hermes')))
37
+ if not hermes_home.is_dir():
38
+ return
39
+ for name in _SENSITIVE_FILES:
40
+ fpath = hermes_home / name
41
+ if not fpath.exists():
42
+ continue
43
+ try:
44
+ current = stat.S_IMODE(fpath.stat().st_mode)
45
+ # If operator declared a mode, allow group bits but still fix world bits
46
+ if declared_mode is not None:
47
+ if current & 0o007: # other bits set (world-readable/writable)
48
+ fpath.chmod(current & ~0o007)
49
+ print(f' [security] removed world bits on {fpath.name} ({oct(current)} -> {oct(current & ~0o007)})', flush=True)
50
+ else:
51
+ if current & 0o077: # group or other bits set
52
+ fpath.chmod(0o600)
53
+ print(f' [security] fixed permissions on {fpath.name} ({oct(current)} -> 0600)', flush=True)
54
+ except OSError:
55
+ pass # best-effort; don't abort startup
56
+
57
+
58
+ def _agent_dir() -> Path | None:
59
+ hermes_home = Path(os.environ.get('HERMES_HOME', str(Path.home() / '.hermes')))
60
+ for raw in [os.environ.get('HERMES_WEBUI_AGENT_DIR', '').strip(), str(hermes_home / 'hermes-agent')]:
61
+ if not raw:
62
+ continue
63
+ p = Path(raw).expanduser()
64
+ if p.is_dir():
65
+ return p.resolve()
66
+ return None
67
+
68
+ def _trusted_agent_dir(agent_dir: Path) -> bool:
69
+ """Return True if agent_dir passes ownership and permission checks.
70
+
71
+ Validates that the directory is not world- or group-writable and,
72
+ on POSIX systems, is owned by the current process user.
73
+
74
+ Intentionally does NOT enforce a canonical path (i.e. does not require
75
+ the dir to be ~/.hermes/hermes-agent), so custom HERMES_WEBUI_AGENT_DIR
76
+ paths work correctly when HERMES_WEBUI_AUTO_INSTALL=1 is set.
77
+ """
78
+ try:
79
+ st = agent_dir.stat()
80
+ if stat.S_IMODE(st.st_mode) & 0o022:
81
+ # World- or group-writable — untrusted
82
+ return False
83
+ if hasattr(os, 'getuid') and st.st_uid != os.getuid():
84
+ # Not owned by current user (POSIX only; Windows fallback skips)
85
+ return False
86
+ return True
87
+ except OSError:
88
+ return False
89
+
90
+
91
+ def auto_install_agent_deps() -> bool:
92
+ enabled = os.environ.get('HERMES_WEBUI_AUTO_INSTALL', '').strip().lower() in ('1', 'true', 'yes')
93
+ if not enabled:
94
+ print('[!!] Auto-install disabled. Set HERMES_WEBUI_AUTO_INSTALL=1 to enable.', flush=True)
95
+ return False
96
+ agent_dir = _agent_dir()
97
+ if agent_dir is None:
98
+ print('[!!] Auto-install skipped: agent directory not found.', flush=True)
99
+ return False
100
+ if not _trusted_agent_dir(agent_dir):
101
+ print('[!!] Auto-install skipped: agent directory failed trust check (check ownership/permissions).', flush=True)
102
+ return False
103
+ req_file = agent_dir / 'requirements.txt'
104
+ pyproject = agent_dir / 'pyproject.toml'
105
+ if req_file.exists():
106
+ install_args = [sys.executable, '-m', 'pip', 'install', '--quiet', '-r', str(req_file)]
107
+ print(f' Installing from {req_file} ...', flush=True)
108
+ elif pyproject.exists():
109
+ install_args = [sys.executable, '-m', 'pip', 'install', '--quiet', str(agent_dir)]
110
+ print(f' Installing from {agent_dir} (pyproject.toml) ...', flush=True)
111
+ else:
112
+ print('[!!] Auto-install skipped: no requirements.txt or pyproject.toml in agent dir.', flush=True)
113
+ return False
114
+ try:
115
+ result = subprocess.run(install_args, capture_output=True, text=True, timeout=120)
116
+ if result.returncode != 0:
117
+ print(f'[!!] pip install failed (exit {result.returncode}):', flush=True)
118
+ for line in (result.stderr or '').splitlines()[-10:]:
119
+ print(f' {line}', flush=True)
120
+ return False
121
+ print('[ok] pip install completed.', flush=True)
122
+ return True
123
+ except subprocess.TimeoutExpired:
124
+ print('[!!] Auto-install timed out after 120s.', flush=True)
125
+ return False
126
+ except Exception as e:
127
+ print(f'[!!] Auto-install error: {e}', flush=True)
128
+ return False
@@ -0,0 +1,187 @@
1
+ """
2
+ Hermes Web UI -- Optional state.db sync bridge.
3
+
4
+ Mirrors WebUI session metadata (token usage, title, model) into the
5
+ hermes-agent state.db so that /insights, session lists, and cost
6
+ tracking include WebUI activity.
7
+
8
+ This is opt-in via the 'sync_to_insights' setting (default: off).
9
+ All operations are wrapped in try/except -- if state.db is unavailable,
10
+ locked, or the schema doesn't match, the WebUI continues normally.
11
+
12
+ The bridge uses absolute token counts (not deltas) because the WebUI
13
+ Session object already accumulates totals across turns. This avoids
14
+ any double-counting risk.
15
+ """
16
+ import logging
17
+ import os
18
+ from pathlib import Path
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _get_state_db(profile: str=None):
24
+ """Get a SessionDB instance for a profile's state.db.
25
+
26
+ When ``profile`` is provided the function resolves *that* profile's
27
+ home directory directly (via ``_resolve_profile_home_for_name``).
28
+ If resolution fails (unknown profile name, IO error, etc.) the
29
+ function returns ``None`` rather than silently falling back to
30
+ ``HERMES_HOME`` — silently routing the write to the wrong DB
31
+ would defeat the point of the explicit-profile path (#2762).
32
+
33
+ When ``profile`` is None it falls back to the TLS-based
34
+ ``get_active_hermes_home()`` lookup for backward compatibility,
35
+ with a final ``HERMES_HOME`` fallback only on that path. TLS may be
36
+ unset in background/worker threads, in which case the lookup falls
37
+ through to the process-global active profile and can write to the
38
+ wrong DB. Callers that know the session's profile (e.g.
39
+ ``sync_session_usage`` after a stream completes on a background
40
+ thread) should pass it explicitly to avoid that race.
41
+
42
+ Returns None if hermes_state is not importable, the explicit
43
+ profile cannot be resolved, or the DB is unavailable. Each caller
44
+ is responsible for calling db.close() when done.
45
+ """
46
+ try:
47
+ from hermes_state import SessionDB
48
+ except ImportError:
49
+ return None
50
+
51
+ if profile is not None:
52
+ # Explicit-profile path — a resolution failure here MUST NOT
53
+ # silently fall back to HERMES_HOME or the caller's "write to
54
+ # the named profile" contract is broken (the original #2762
55
+ # symptom: writes leaking into the wrong profile's state.db).
56
+ #
57
+ # Defense-in-depth (per #2827 maintainer review): validate the
58
+ # name shape BEFORE handing it to ``_resolve_profile_home_for_name``.
59
+ # The resolver itself rarely raises — for an invalid-but-non-
60
+ # malicious name (e.g. one that fails ``_PROFILE_ID_RE``) it
61
+ # quietly returns ``_DEFAULT_HERMES_HOME``, which is the exact
62
+ # leak we're trying to prevent on the explicit-profile path.
63
+ # Validating up-front turns that quiet leak into an explicit
64
+ # "refuse + log + return None" so the contract is "write to
65
+ # the EXACT named profile, or write nowhere."
66
+ try:
67
+ from api.profiles import (
68
+ _resolve_profile_home_for_name,
69
+ _PROFILE_ID_RE,
70
+ _is_root_profile,
71
+ )
72
+ if not (_is_root_profile(profile) or _PROFILE_ID_RE.fullmatch(profile)):
73
+ logger.warning(
74
+ "state_sync: refusing invalid profile name %r — skipping "
75
+ "write rather than leaking to the default state.db (#2762).",
76
+ profile,
77
+ )
78
+ return None
79
+ hermes_home = Path(_resolve_profile_home_for_name(profile)).expanduser().resolve()
80
+ except Exception:
81
+ logger.warning(
82
+ "state_sync: could not resolve profile %r — skipping write rather "
83
+ "than leaking to the active profile (#2762).", profile,
84
+ )
85
+ return None
86
+ else:
87
+ # Implicit / TLS-fallback path — preserves pre-#2762 behavior
88
+ # for any caller that doesn't pass profile= explicitly.
89
+ try:
90
+ from api.profiles import get_active_hermes_home
91
+ hermes_home = Path(get_active_hermes_home()).expanduser().resolve()
92
+ except Exception:
93
+ logger.debug("Failed to resolve hermes home, using default")
94
+ hermes_home = Path(os.getenv('HERMES_HOME', str(Path.home() / '.hermes')))
95
+
96
+ db_path = hermes_home / 'state.db'
97
+ if not db_path.exists():
98
+ return None
99
+
100
+ try:
101
+ return SessionDB(db_path)
102
+ except Exception:
103
+ logger.debug("Failed to open state.db")
104
+ return None
105
+
106
+
107
+ def sync_session_start(session_id: str, model=None, profile: str=None) -> None:
108
+ """Register a WebUI session in state.db (idempotent).
109
+ Called when a session's first message is sent.
110
+
111
+ ``profile`` lets the caller name the target state.db explicitly,
112
+ avoiding the TLS-vs-background-thread mismatch in #2762. When
113
+ omitted, the active profile is resolved from TLS (then process
114
+ globals) as before.
115
+ """
116
+ db = _get_state_db(profile=profile)
117
+ if not db:
118
+ return
119
+ try:
120
+ db.ensure_session(
121
+ session_id=session_id,
122
+ source='webui',
123
+ model=model,
124
+ )
125
+ except Exception:
126
+ logger.debug("Failed to sync session start to state.db")
127
+ finally:
128
+ try:
129
+ db.close()
130
+ except Exception:
131
+ logger.debug("Failed to close state.db")
132
+
133
+
134
+ def sync_session_usage(session_id: str, input_tokens: int=0, output_tokens: int=0,
135
+ estimated_cost=None, model=None, title: str=None,
136
+ message_count: int=None, profile: str=None) -> None:
137
+ """Update token usage and title for a WebUI session in state.db.
138
+ Called after each turn completes. Uses absolute=True to set totals
139
+ (the WebUI Session already accumulates across turns).
140
+
141
+ ``profile`` lets the caller name the target state.db explicitly,
142
+ which is what fixes #2762: this function is invoked from the
143
+ agent streaming worker thread, where the request-thread's TLS
144
+ profile context has not been propagated. Without an explicit
145
+ profile, the TLS lookup falls back to the process-global active
146
+ profile and writes the session's usage to the wrong state.db
147
+ (e.g. ``hiyuki``'s instead of the cookie-switched ``maiko``'s).
148
+ """
149
+ db = _get_state_db(profile=profile)
150
+ if not db:
151
+ return
152
+ try:
153
+ # Ensure session exists first (idempotent)
154
+ db.ensure_session(session_id=session_id, source='webui', model=model)
155
+ # Set absolute token counts
156
+ db.update_token_counts(
157
+ session_id=session_id,
158
+ input_tokens=input_tokens,
159
+ output_tokens=output_tokens,
160
+ estimated_cost_usd=estimated_cost,
161
+ model=model,
162
+ absolute=True,
163
+ )
164
+ # Update title if we have one, using the public API
165
+ if title:
166
+ try:
167
+ db.set_session_title(session_id, title)
168
+ except Exception:
169
+ logger.debug("Failed to sync session title to state.db")
170
+ # Update message count
171
+ if message_count is not None:
172
+ try:
173
+ def _set_msg_count(conn):
174
+ conn.execute(
175
+ "UPDATE sessions SET message_count = ? WHERE id = ?",
176
+ (message_count, session_id),
177
+ )
178
+ db._execute_write(_set_msg_count)
179
+ except Exception:
180
+ logger.debug("Failed to sync message count to state.db")
181
+ except Exception:
182
+ logger.debug("Failed to sync session usage to state.db")
183
+ finally:
184
+ try:
185
+ db.close()
186
+ except Exception:
187
+ logger.debug("Failed to close state.db")