@bitseek/hermes-webui 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/bin/hermes-webui.mjs +588 -0
- package/package.json +25 -0
- package/scripts/sync-vendor.mjs +74 -0
- package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
- package/templates/systemd/hermes-webui.service +13 -0
- package/templates/windows/hermes-webui-task.ps1 +3 -0
- package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
- package/vendor/agent-frontend-shell/.dockerignore +7 -0
- package/vendor/agent-frontend-shell/.env.docker.example +89 -0
- package/vendor/agent-frontend-shell/.env.example +34 -0
- package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
- package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
- package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
- package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
- package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
- package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
- package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
- package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
- package/vendor/agent-frontend-shell/AGENTS.md +80 -0
- package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
- package/vendor/agent-frontend-shell/BUGS.md +52 -0
- package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
- package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
- package/vendor/agent-frontend-shell/DESIGN.md +173 -0
- package/vendor/agent-frontend-shell/Dockerfile +91 -0
- package/vendor/agent-frontend-shell/LICENSE +21 -0
- package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
- package/vendor/agent-frontend-shell/README.md +705 -0
- package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
- package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
- package/vendor/agent-frontend-shell/TESTING.md +1932 -0
- package/vendor/agent-frontend-shell/THEMES.md +170 -0
- package/vendor/agent-frontend-shell/api/__init__.py +1 -0
- package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
- package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
- package/vendor/agent-frontend-shell/api/auth.py +592 -0
- package/vendor/agent-frontend-shell/api/background.py +87 -0
- package/vendor/agent-frontend-shell/api/clarify.py +238 -0
- package/vendor/agent-frontend-shell/api/commands.py +124 -0
- package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +5178 -0
- package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
- package/vendor/agent-frontend-shell/api/extensions.py +253 -0
- package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
- package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
- package/vendor/agent-frontend-shell/api/goals.py +608 -0
- package/vendor/agent-frontend-shell/api/helpers.py +474 -0
- package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
- package/vendor/agent-frontend-shell/api/metering.py +194 -0
- package/vendor/agent-frontend-shell/api/models.py +4210 -0
- package/vendor/agent-frontend-shell/api/oauth.py +770 -0
- package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
- package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
- package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
- package/vendor/agent-frontend-shell/api/providers.py +2175 -0
- package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
- package/vendor/agent-frontend-shell/api/rollback.py +320 -0
- package/vendor/agent-frontend-shell/api/routes.py +13990 -0
- package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
- package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
- package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
- package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
- package/vendor/agent-frontend-shell/api/session_events.py +45 -0
- package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
- package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
- package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
- package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
- package/vendor/agent-frontend-shell/api/startup.py +128 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
- package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
- package/vendor/agent-frontend-shell/api/system_health.py +167 -0
- package/vendor/agent-frontend-shell/api/terminal.py +410 -0
- package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
- package/vendor/agent-frontend-shell/api/updates.py +1261 -0
- package/vendor/agent-frontend-shell/api/upload.py +322 -0
- package/vendor/agent-frontend-shell/api/usage.py +26 -0
- package/vendor/agent-frontend-shell/api/workspace.py +867 -0
- package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
- package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
- package/vendor/agent-frontend-shell/bootstrap.py +492 -0
- package/vendor/agent-frontend-shell/ctl.sh +427 -0
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
- package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
- package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
- package/vendor/agent-frontend-shell/docker_init.bash +459 -0
- package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
- package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
- package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
- package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
- package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
- package/vendor/agent-frontend-shell/docs/docker.md +337 -0
- package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
- package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
- package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
- package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
- package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
- package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
- package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
- package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
- package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
- package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
- package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
- package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
- package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
- package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
- package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
- package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
- package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
- package/vendor/agent-frontend-shell/extensions/index.js +67 -0
- package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
- package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
- package/vendor/agent-frontend-shell/install.sh +63 -0
- package/vendor/agent-frontend-shell/mcp_server.py +567 -0
- package/vendor/agent-frontend-shell/package.json +12 -0
- package/vendor/agent-frontend-shell/pyproject.toml +56 -0
- package/vendor/agent-frontend-shell/pytest.ini +3 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +624 -0
- package/vendor/agent-frontend-shell/start.ps1 +210 -0
- package/vendor/agent-frontend-shell/start.sh +65 -0
- package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/static/boot.js +1990 -0
- package/vendor/agent-frontend-shell/static/commands.js +1402 -0
- package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
- package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
- package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
- package/vendor/agent-frontend-shell/static/icons.js +92 -0
- package/vendor/agent-frontend-shell/static/index.html +1506 -0
- package/vendor/agent-frontend-shell/static/login.js +177 -0
- package/vendor/agent-frontend-shell/static/manifest.json +53 -0
- package/vendor/agent-frontend-shell/static/messages.js +3521 -0
- package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
- package/vendor/agent-frontend-shell/static/panels.js +7995 -0
- package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
- package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
- package/vendor/agent-frontend-shell/static/style.css +4774 -0
- package/vendor/agent-frontend-shell/static/sw.js +173 -0
- package/vendor/agent-frontend-shell/static/terminal.js +632 -0
- package/vendor/agent-frontend-shell/static/ui.js +8997 -0
- package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
- package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
- package/vendor/agent-frontend-shell/static/workspace.js +680 -0
|
@@ -0,0 +1,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")
|