diff --git a/.gitignore b/.gitignore index 7c6fec3..4adc34d 100644 --- a/.gitignore +++ b/.gitignore @@ -261,6 +261,11 @@ frontend/.desktop-export-stash-*/ backend/data/wormhole_stderr.log backend/data/wormhole_stdout.log +# Hermes Agent (operator-local runtime install — not project source) +.hermes/ +**/.hermes/ +hermes-agent/ + # Runtime caches that already slip through the backend/data/* blanket # (these are caught by the wildcard but listing for clarity) diff --git a/backend/Dockerfile b/backend/Dockerfile index bdb9868..5c47cf9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -27,6 +27,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ + git \ tor \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ @@ -72,7 +73,7 @@ ENV PRIVACY_CORE_LIB=/app/libprivacy_core.so # Create a non-root user for security # Grant write access to /app so the auto-updater can extract files # Pre-create /app/data so mounted volumes inherit correct ownership -RUN adduser --system --uid 1001 backenduser \ +RUN adduser --system --uid 1001 --home /app backenduser \ && mkdir -p /app/data \ && chown -R backenduser /app \ && chmod -R u+w /app diff --git a/backend/routers/agent_shell.py b/backend/routers/agent_shell.py index 56ddf6b..bc95cc4 100644 --- a/backend/routers/agent_shell.py +++ b/backend/routers/agent_shell.py @@ -43,9 +43,32 @@ def _set_winsize(fd: int, rows: int, cols: int) -> None: fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize) +def _published_local_dashboard_ws(ws: WebSocket) -> bool: + """Browser → published Docker port appears as a bridge IP, not loopback. + + For the operator shell only, also accept when the upgrade request clearly + targets the local dashboard (Host/Origin on localhost). + """ + host_header = str(ws.headers.get("host") or "").strip().lower() + host_name = host_header.split(":", 1)[0] + if host_name in {"127.0.0.1", "localhost", "::1"}: + return True + + origin = str(ws.headers.get("origin") or "").strip().lower() + if origin.startswith("http://127.0.0.1:") or origin.startswith("http://localhost:"): + return True + if origin.startswith("https://127.0.0.1:") or origin.startswith("https://localhost:"): + return True + return False + + async def _authorize_agent_shell_ws(ws: WebSocket, admin_key_query: str = "") -> None: host = (ws.client.host or "").lower() if ws.client else "" - if _is_trusted_local_runtime_host(host) or (_debug_mode_enabled() and host == "test"): + if ( + _is_trusted_local_runtime_host(host) + or _published_local_dashboard_ws(ws) + or (_debug_mode_enabled() and host == "test") + ): return admin_key = _current_admin_key() presented = str(admin_key_query or ws.headers.get("x-admin-key", "") or "").strip() @@ -164,6 +187,18 @@ async def agent_shell_websocket( env = os.environ.copy() env.setdefault("TERM", "xterm-256color") env.setdefault("COLORTERM", "truecolor") + home = shell_cwd if os.path.isdir(shell_cwd) else "/app" + env["HOME"] = home + env["USER"] = env.get("USER") or "operator" + path_prefixes = [ + os.path.join(home, ".local", "bin"), + os.path.join(home, ".hermes", "bin"), + ] + path = env.get("PATH", "") + for prefix in path_prefixes: + if os.path.isdir(prefix): + path = f"{prefix}:{path}" if path else prefix + env["PATH"] = path proc = await asyncio.create_subprocess_exec( shell, diff --git a/backend/services/agent_shell_settings.py b/backend/services/agent_shell_settings.py index 3fd50e2..018c575 100644 --- a/backend/services/agent_shell_settings.py +++ b/backend/services/agent_shell_settings.py @@ -16,7 +16,13 @@ _LOCK = threading.Lock() def _default_working_directory() -> str: - return os.environ.get("AGENT_SHELL_DEFAULT_CWD") or os.environ.get("HOME") or "/app" + explicit = str(os.environ.get("AGENT_SHELL_DEFAULT_CWD") or "").strip() + if explicit and os.path.isdir(explicit): + return explicit + home = str(os.environ.get("HOME") or "").strip() + if home and home != "/nonexistent" and os.path.isdir(home): + return home + return "/app" def get_agent_shell_settings() -> dict[str, Any]: diff --git a/desktop-shell/tauri-skeleton/build.ps1 b/desktop-shell/tauri-skeleton/build.ps1 index a250fd8..9811cb6 100644 --- a/desktop-shell/tauri-skeleton/build.ps1 +++ b/desktop-shell/tauri-skeleton/build.ps1 @@ -132,7 +132,7 @@ try { ) { $env:TAURI_SIGNING_PRIVATE_KEY = Get-Content -LiteralPath $localUpdaterKey -Raw if (($null -eq $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD) -and (Test-Path $localUpdaterKeyPassword)) { - $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = Get-Content -LiteralPath $localUpdaterKeyPassword -Raw + $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = (Get-Content -LiteralPath $localUpdaterKeyPassword -Raw).Trim() } } diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..d3a62f3 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,10 @@ +# Auto-loaded by `docker compose` — build from local source instead of pulling stale GHCR images. +services: + backend: + build: + context: . + dockerfile: ./backend/Dockerfile + + frontend: + build: + context: ./frontend diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 0a67e19..76d858a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -615,7 +615,7 @@ export default function Dashboard() { )} - {/* 2. MESH CHAT (Middle) */} + {/* 2. MESHTASTIC CHAT (Middle) */} {secondaryBootReady && (
+ + Connect your own agent CLIs here — OpenClaw, Codex, Gemini, or whatever you run locally. + +
+ ++ + The session opens in your Shadowbroker workspace by default. Use it for repo scripts, mesh + + tools, or any terminal workflow you already rely on. + +
+ + ++ Obfuscated Wormhole lane for the Infonet shell. Leave this tab to shut Wormhole down. +
+ {status && !error && ( +