@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,322 @@
1
+ """
2
+ Hermes Web UI -- File upload: multipart parser and upload handler.
3
+ """
4
+ import mimetypes
5
+ import os
6
+ import re as _re
7
+ import email.parser
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from api.config import MAX_UPLOAD_BYTES, STATE_DIR
12
+ from api.helpers import j, bad
13
+ from api.models import get_session
14
+ from api.workspace import safe_resolve_ws
15
+
16
+ _MAX_EXTRACTED_BYTES = 10 * MAX_UPLOAD_BYTES
17
+
18
+
19
+ def parse_multipart(rfile, content_type, content_length) -> tuple:
20
+ import re as _re, email.parser as _ep
21
+ m = _re.search(r'boundary=([^;\s]+)', content_type)
22
+ if not m:
23
+ raise ValueError('No boundary in Content-Type')
24
+ boundary = m.group(1).strip('"').encode()
25
+ raw = rfile.read(content_length)
26
+ fields = {}
27
+ files = {}
28
+ delimiter = b'--' + boundary
29
+ end_marker = b'--' + boundary + b'--'
30
+ parts = raw.split(delimiter)
31
+ for part in parts[1:]:
32
+ stripped = part.lstrip(b'\r\n')
33
+ if stripped.startswith(b'--'):
34
+ break
35
+ sep = b'\r\n\r\n' if b'\r\n\r\n' in part else b'\n\n'
36
+ if sep not in part:
37
+ continue
38
+ header_raw, body = part.split(sep, 1)
39
+ if body.endswith(b'\r\n'):
40
+ body = body[:-2]
41
+ elif body.endswith(b'\n'):
42
+ body = body[:-1]
43
+ header_text = header_raw.lstrip(b'\r\n').decode('utf-8', errors='replace')
44
+ msg = _ep.HeaderParser().parsestr(header_text)
45
+ disp = msg.get('Content-Disposition', '')
46
+ name_m = _re.search(r'name="([^"]*)"', disp)
47
+ file_m = _re.search(r'filename="([^"]*)"', disp)
48
+ if not name_m:
49
+ continue
50
+ name = name_m.group(1)
51
+ if file_m:
52
+ files[name] = (file_m.group(1), body)
53
+ else:
54
+ fields[name] = body.decode('utf-8', errors='replace')
55
+ return fields, files
56
+
57
+
58
+ def _sanitize_upload_name(filename: str) -> str:
59
+ safe_name = _re.sub(r'[^\w.\-]', '_', Path(filename).name)[:200]
60
+ if not safe_name or safe_name.strip('.') == '':
61
+ raise ValueError('Invalid filename')
62
+ return safe_name
63
+
64
+
65
+ def _attachment_root() -> Path:
66
+ """Return the configured upload inbox root.
67
+
68
+ Plain chat attachments are transient context for the agent, not project
69
+ source files. Keep them out of the active workspace by default while still
70
+ allowing operators to move the inbox with HERMES_WEBUI_ATTACHMENT_DIR.
71
+ """
72
+ override = os.getenv('HERMES_WEBUI_ATTACHMENT_DIR', '').strip()
73
+ if override:
74
+ return Path(override).expanduser().resolve()
75
+ return (STATE_DIR / 'attachments').resolve()
76
+
77
+
78
+ def _upload_destination(session_id: str, safe_name: str) -> Path:
79
+ dest_dir = _session_attachment_dir(session_id)
80
+ dest_dir.mkdir(parents=True, exist_ok=True)
81
+ dest = (dest_dir / safe_name).resolve()
82
+ if not dest.is_relative_to(dest_dir):
83
+ raise ValueError('Invalid upload destination')
84
+ if dest.exists():
85
+ stem = dest.stem
86
+ suffix = dest.suffix
87
+ for idx in range(1, 1000):
88
+ candidate = (dest_dir / f'{stem}-{idx}{suffix}').resolve()
89
+ if not candidate.is_relative_to(dest_dir):
90
+ raise ValueError('Invalid upload destination')
91
+ if not candidate.exists():
92
+ return candidate
93
+ raise ValueError('Too many uploads with the same filename')
94
+ return dest
95
+
96
+
97
+ def _session_attachment_dir(session_id: str, *, root: Path | None = None) -> Path:
98
+ root = (root or _attachment_root()).resolve()
99
+ dest_dir = (root / _re.sub(r'[^\w.\-]', '_', str(session_id or 'session'))[:120]).resolve()
100
+ if not dest_dir.is_relative_to(root):
101
+ raise ValueError('Invalid attachment directory')
102
+ return dest_dir
103
+
104
+
105
+ def handle_upload(handler):
106
+ import traceback as _tb
107
+ try:
108
+ content_type = handler.headers.get('Content-Type', '')
109
+ content_length = int(handler.headers.get('Content-Length', 0) or 0)
110
+ if content_length > MAX_UPLOAD_BYTES:
111
+ return j(handler, {'error': f'File too large (max {MAX_UPLOAD_BYTES//1024//1024}MB)'}, status=413)
112
+ fields, files = parse_multipart(handler.rfile, content_type, content_length)
113
+ session_id = fields.get('session_id', '')
114
+ if 'file' not in files:
115
+ return j(handler, {'error': 'No file field in request'}, status=400)
116
+ filename, file_bytes = files['file']
117
+ if not filename:
118
+ return j(handler, {'error': 'No filename in upload'}, status=400)
119
+ try:
120
+ s = get_session(session_id)
121
+ except KeyError:
122
+ return j(handler, {'error': 'Session not found'}, status=404)
123
+ safe_name = _sanitize_upload_name(filename)
124
+ dest = _upload_destination(session_id, safe_name)
125
+ dest.write_bytes(file_bytes)
126
+ mime = mimetypes.guess_type(safe_name)[0] or 'application/octet-stream'
127
+ return j(handler, {
128
+ 'filename': dest.name,
129
+ 'path': str(dest),
130
+ 'size': dest.stat().st_size,
131
+ 'mime': mime,
132
+ 'is_image': mime.startswith('image/'),
133
+ })
134
+ except ValueError as e:
135
+ return j(handler, {'error': str(e)}, status=400)
136
+ except Exception:
137
+ print('[webui] upload error: ' + _tb.format_exc(), flush=True)
138
+ return j(handler, {'error': 'Upload failed'}, status=500)
139
+
140
+
141
+ def extract_archive(file_bytes: bytes, filename: str, workspace: Path):
142
+ """Extract a zip or tar archive into the workspace.
143
+
144
+ Returns a dict with ``extracted`` (int), ``files`` (list[str]).
145
+ Raises ValueError on zip-slip or unsupported format.
146
+ """
147
+ import zipfile, tarfile, io, os, shutil
148
+
149
+ name = Path(filename).name
150
+ stem = Path(filename).stem # strip .zip / .tar.gz etc.
151
+
152
+ if name.lower().endswith(('.zip',)):
153
+ _mode = 'zip'
154
+ elif name.lower().endswith(('.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz')):
155
+ _mode = 'tar'
156
+ else:
157
+ raise ValueError(f'Unsupported archive format: {filename}')
158
+
159
+ # Determine destination directory — use archive stem as folder name
160
+ dest_dir = safe_resolve_ws(workspace, stem)
161
+ # Avoid overwriting existing files by appending a suffix
162
+ if dest_dir.exists():
163
+ import string, random
164
+ while dest_dir.exists():
165
+ suffix = ''.join(random.choices(string.digits, k=3))
166
+ dest_dir = dest_dir.with_name(stem + '_' + suffix)
167
+ dest_dir.mkdir(parents=True, exist_ok=True)
168
+
169
+ extracted_files = []
170
+ total_extracted = 0
171
+
172
+ try:
173
+ if _mode == 'zip':
174
+ with zipfile.ZipFile(io.BytesIO(file_bytes)) as zf:
175
+ for member in zf.infolist():
176
+ # Skip directories
177
+ if member.is_dir():
178
+ continue
179
+ # Zip-slip protection
180
+ member_path = (dest_dir / member.filename).resolve()
181
+ if not member_path.is_relative_to(dest_dir.resolve()):
182
+ raise ValueError(f'Zip-slip blocked: {member.filename}')
183
+ # Zip-bomb protection: track actual extracted bytes (not declared file_size)
184
+ if total_extracted > _MAX_EXTRACTED_BYTES:
185
+ raise ValueError(
186
+ f'Extraction too large ({total_extracted // (1024*1024)} MB > '
187
+ f'{_MAX_EXTRACTED_BYTES // (1024*1024)} MB limit). '
188
+ f'Possible zip bomb.'
189
+ )
190
+ member_path.parent.mkdir(parents=True, exist_ok=True)
191
+ with zf.open(member) as src, open(member_path, 'wb') as dst:
192
+ _chunk_size = 65536
193
+ while True:
194
+ chunk = src.read(_chunk_size)
195
+ if not chunk:
196
+ break
197
+ total_extracted += len(chunk)
198
+ if total_extracted > _MAX_EXTRACTED_BYTES:
199
+ raise ValueError(
200
+ f'Extraction too large (> '
201
+ f'{_MAX_EXTRACTED_BYTES // (1024*1024)} MB limit). '
202
+ f'Possible zip bomb.'
203
+ )
204
+ dst.write(chunk)
205
+ extracted_files.append(str(member_path.relative_to(workspace.resolve())))
206
+
207
+ elif _mode == 'tar':
208
+ with tarfile.open(fileobj=io.BytesIO(file_bytes)) as tf:
209
+ for member in tf.getmembers():
210
+ if not member.isfile():
211
+ continue
212
+ # Tar-slip protection
213
+ member_path = (dest_dir / member.name).resolve()
214
+ if not member_path.is_relative_to(dest_dir.resolve()):
215
+ raise ValueError(f'Tar-slip blocked: {member.name}')
216
+ # Tar-bomb protection: track actual extracted bytes (not declared size)
217
+ if total_extracted > _MAX_EXTRACTED_BYTES:
218
+ raise ValueError(
219
+ f'Extraction too large ({total_extracted // (1024*1024)} MB > '
220
+ f'{_MAX_EXTRACTED_BYTES // (1024*1024)} MB limit). '
221
+ f'Possible zip bomb.'
222
+ )
223
+ member_path.parent.mkdir(parents=True, exist_ok=True)
224
+ src_obj = tf.extractfile(member)
225
+ if src_obj:
226
+ with src_obj as src, open(member_path, 'wb') as dst:
227
+ _chunk_size = 65536
228
+ while True:
229
+ chunk = src.read(_chunk_size)
230
+ if not chunk:
231
+ break
232
+ total_extracted += len(chunk)
233
+ if total_extracted > _MAX_EXTRACTED_BYTES:
234
+ raise ValueError(
235
+ f'Extraction too large (> '
236
+ f'{_MAX_EXTRACTED_BYTES // (1024*1024)} MB limit). '
237
+ f'Possible zip bomb.'
238
+ )
239
+ dst.write(chunk)
240
+ extracted_files.append(str(member_path.relative_to(workspace.resolve())))
241
+ except Exception:
242
+ # Clean up partially-extracted directory to avoid orphaned folders
243
+ try:
244
+ shutil.rmtree(dest_dir, ignore_errors=True)
245
+ except Exception:
246
+ pass
247
+ raise
248
+
249
+ return {'extracted': len(extracted_files), 'files': extracted_files, 'dest': str(dest_dir)}
250
+
251
+
252
+ def handle_upload_extract(handler):
253
+ """Handle archive upload and extraction."""
254
+ import traceback as _tb
255
+ try:
256
+ content_type = handler.headers.get('Content-Type', '')
257
+ content_length = int(handler.headers.get('Content-Length', 0) or 0)
258
+ if content_length > MAX_UPLOAD_BYTES:
259
+ return j(handler, {'error': f'File too large (max {MAX_UPLOAD_BYTES//1024//1024}MB)'}, status=413)
260
+ fields, files = parse_multipart(handler.rfile, content_type, content_length)
261
+ session_id = fields.get('session_id', '')
262
+ if 'file' not in files:
263
+ return j(handler, {'error': 'No file field in request'}, status=400)
264
+ filename, file_bytes = files['file']
265
+ if not filename:
266
+ return j(handler, {'error': 'No filename in upload'}, status=400)
267
+ try:
268
+ s = get_session(session_id)
269
+ except KeyError:
270
+ return j(handler, {'error': 'Session not found'}, status=404)
271
+ session_dir = _session_attachment_dir(session_id)
272
+ session_dir.mkdir(parents=True, exist_ok=True)
273
+ result = extract_archive(file_bytes, filename, session_dir)
274
+ return j(handler, {'ok': True, **result})
275
+ except ValueError as e:
276
+ return j(handler, {'error': str(e)}, status=400)
277
+ except Exception:
278
+ print('[webui] upload extract error: ' + _tb.format_exc(), flush=True)
279
+ return j(handler, {'error': 'Archive extraction failed'}, status=500)
280
+
281
+
282
+ def handle_transcribe(handler):
283
+ import traceback as _tb
284
+ temp_path = None
285
+ try:
286
+ content_type = handler.headers.get('Content-Type', '')
287
+ content_length = int(handler.headers.get('Content-Length', 0) or 0)
288
+ if content_length > MAX_UPLOAD_BYTES:
289
+ return j(handler, {'error': f'File too large (max {MAX_UPLOAD_BYTES//1024//1024}MB)'}, status=413)
290
+ fields, files = parse_multipart(handler.rfile, content_type, content_length)
291
+ if 'file' not in files:
292
+ return j(handler, {'error': 'No file field in request'}, status=400)
293
+ filename, file_bytes = files['file']
294
+ if not filename:
295
+ return j(handler, {'error': 'No filename in upload'}, status=400)
296
+ safe_name = _sanitize_upload_name(filename)
297
+ suffix = Path(safe_name).suffix or '.webm'
298
+ with tempfile.NamedTemporaryFile(prefix='webui-stt-', suffix=suffix, delete=False) as tmp:
299
+ temp_path = tmp.name
300
+ tmp.write(file_bytes)
301
+ try:
302
+ from tools.transcription_tools import transcribe_audio
303
+ except ImportError:
304
+ return j(handler, {'error': 'Speech-to-text is unavailable on this server'}, status=503)
305
+ result = transcribe_audio(temp_path)
306
+ if not result.get('success'):
307
+ msg = str(result.get('error') or 'Transcription failed')
308
+ status = 503 if 'unavailable' in msg.lower() or 'not configured' in msg.lower() else 400
309
+ return j(handler, {'error': msg}, status=status)
310
+ transcript = str(result.get('transcript') or '').strip()
311
+ return j(handler, {'ok': True, 'transcript': transcript})
312
+ except ValueError as e:
313
+ return j(handler, {'error': str(e)}, status=400)
314
+ except Exception:
315
+ print('[webui] transcribe error: ' + _tb.format_exc(), flush=True)
316
+ return j(handler, {'error': 'Transcription failed'}, status=500)
317
+ finally:
318
+ if temp_path:
319
+ try:
320
+ Path(temp_path).unlink(missing_ok=True)
321
+ except Exception:
322
+ pass
@@ -0,0 +1,26 @@
1
+ """Usage metric helpers for WebUI display payloads.
2
+
3
+ Prompt-cache hit percentage is cached prompt reads over the full prompt total
4
+ (input + cache reads + cache writes). Keep this calculation in the backend so
5
+ browser display code cannot drift across context indicator and per-turn labels.
6
+ """
7
+
8
+
9
+ def _to_int(value) -> int:
10
+ try:
11
+ return int(value or 0)
12
+ except (TypeError, ValueError):
13
+ return 0
14
+
15
+
16
+ def prompt_cache_hit_percent(cache_read_tokens, prompt_tokens):
17
+ """Return cached reads as a percent of full prompt-token total.
18
+
19
+ ``prompt_tokens`` must include ordinary input, cache reads, and cache writes
20
+ (matching Agent's ``session_prompt_tokens`` value).
21
+ """
22
+ cache_read = _to_int(cache_read_tokens)
23
+ prompt = _to_int(prompt_tokens)
24
+ if cache_read <= 0 or prompt <= 0:
25
+ return None
26
+ return min(100, round((cache_read / prompt) * 100))