diff --git a/docs/mac/remote.md b/docs/mac/remote.md index 3fb9be4d9..14ddc5d9a 100644 --- a/docs/mac/remote.md +++ b/docs/mac/remote.md @@ -23,8 +23,8 @@ This flow lets the macOS app act as a full remote control for a Clawdis relay ru 4) Health checks and Web Chat will now run through this SSH tunnel automatically. ## Web Chat over SSH -- The relay hosts a loopback-only HTTP server (`clawdis webchat --port `; default 18788). -- The mac app forwards `127.0.0.1:` over SSH (`ssh -L :127.0.0.1:`), loads `/webchat/info`, and serves the Web Chat UI in-app. +- The relay hosts a loopback-only HTTP server (default 18788, see `webchat.port`). +- The mac app forwards `127.0.0.1:` over SSH (`ssh -L :127.0.0.1:`), then loads `/webchat/?session=` in-app. - Keep the feature enabled in *Settings → Config → Web chat*. Disable it to hide the menu entry entirely. ## Permissions @@ -38,7 +38,7 @@ This flow lets the macOS app act as a full remote control for a Clawdis relay ru ## Troubleshooting - **exit 127 / not found**: `clawdis` isn’t on PATH for non-login shells. Add it to `/etc/paths`, your shell rc, or symlink into `/usr/local/bin`/`/opt/homebrew/bin`. - **Health probe failed**: check SSH reachability, PATH, and that Baileys is logged in (`clawdis status --json`). -- **Web Chat stuck**: confirm the remote webchat server is running (`clawdis webchat --json`) and the port matches *Settings → Config*. +- **Web Chat stuck**: confirm the relay is running on the remote host and `webchat.enabled` is true; ensure the forwarded port matches *Settings → Config*. - **Voice Wake**: trigger phrases are forwarded automatically in remote mode; no separate forwarder is needed. ## Notification sounds diff --git a/docs/webchat.md b/docs/webchat.md index e614d0608..d85a62225 100644 --- a/docs/webchat.md +++ b/docs/webchat.md @@ -3,19 +3,18 @@ Updated: 2025-12-08 ## What shipped -- A lightweight HTTP server now lives inside the Node relay (`clawdis webchat --port 18788`). -- It binds to **127.0.0.1** only and serves: - - `GET /webchat/info?session=` → `{port, token, sessionId, initialMessages, basePath}` plus history from the relay’s session store. +- The relay now starts a loopback-only web chat server automatically (default port **18788**, configurable via `webchat.port`). +- Endpoints: + - `GET /webchat/info?session=` → `{port, sessionId, initialMessages, basePath}` plus history from the relay’s session store. - `GET /webchat/*` → static Web Chat assets. - `POST /webchat/rpc` → runs `clawdis agent --json` and returns `{ ok, payloads?, error? }`. -- The macOS app embeds this UI in a WKWebView. In **remote mode** it first opens an SSH tunnel (`ssh -L :127.0.0.1:`) to the remote host, then loads `/webchat/info` through that tunnel. -- Initial messages are preloaded from the relay’s session store, so remote sessions appear immediately. -- Sending now goes over the HTTP `/webchat/rpc` endpoint (no more AgentRPC fallback). -- Feature flag + port live in *Settings → Config → Web chat*. When disabled, the “Open Chat” menu entry is hidden. +- The macOS app simply loads `http://127.0.0.1:/webchat/?session=` (or the SSH-forwarded port in remote mode). No Swift bridge is used for sends; all chat traffic stays inside the Node relay. +- Initial messages are fetched from `/webchat/info`, so history appears immediately. +- Enable/disable via `webchat.enabled` (default **true**); set the port with `webchat.port`. ## Security - Loopback only; remote access requires SSH port-forwarding. -- Optional bearer token support is wired; tokens are returned by `/webchat/info` and accepted by `/webchat/rpc`. +- No bearer token; the trust model is “local machine or your SSH tunnel”. ## Failure handling - Bootstrap errors show in-app (“Web chat failed to connect …”) instead of hanging. @@ -24,7 +23,7 @@ Updated: 2025-12-08 ## Dev notes - Static assets stay in `apps/macos/Sources/Clawdis/Resources/WebChat`; the server reads them directly. - Server code: `src/webchat/server.ts`. -- CLI entrypoint: `clawdis webchat --json [--port N]`. +- CLI entrypoint (optional): `clawdis webchat --json [--port N]` to query/start manually. - Mac glue: `WebChatWindow.swift` (bootstrap + tunnel) and `WebChatTunnel` (SSH -L). ## TODO / nice-to-haves diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index ffd2b3115..8048045c0 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -7,7 +7,8 @@ const monitorWebProvider = vi.fn(); const logWebSelfId = vi.fn(); const waitForever = vi.fn(); const monitorTelegramProvider = vi.fn(); -const startWebChatServer = vi.fn(async () => ({ port: 18788, token: null })); +const startWebChatServer = vi.fn(async () => ({ port: 18788 })); +const ensureWebChatServerFromConfig = vi.fn(async () => ({ port: 18788 })); const runtime = { log: vi.fn(), @@ -29,6 +30,7 @@ vi.mock("../telegram/monitor.js", () => ({ })); vi.mock("../webchat/server.js", () => ({ startWebChatServer, + ensureWebChatServerFromConfig, getWebChatServer: () => null, })); vi.mock("./deps.js", () => ({ @@ -104,7 +106,7 @@ describe("cli program", () => { await program.parseAsync(["webchat", "--json"], { from: "user" }); expect(startWebChatServer).toHaveBeenCalled(); expect(runtime.log).toHaveBeenCalledWith( - JSON.stringify({ port: 18788, token: null, basePath: "/webchat/", host: "127.0.0.1" }), + JSON.stringify({ port: 18788, basePath: "/webchat/", host: "127.0.0.1" }), ); }); }); diff --git a/src/cli/program.ts b/src/cli/program.ts index 0ecb148d4..c61907615 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -17,7 +17,11 @@ import { setHeartbeatsEnabled, type WebMonitorTuning, } from "../provider-web.js"; -import { startWebChatServer, getWebChatServer } from "../webchat/server.js"; +import { + startWebChatServer, + getWebChatServer, + ensureWebChatServerFromConfig, +} from "../webchat/server.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { VERSION } from "../version.js"; import { @@ -509,6 +513,16 @@ Examples: ), ); try { + // Start loopback web chat server unless disabled. + const webchatServer = await ensureWebChatServerFromConfig(); + if (webchatServer) { + defaultRuntime.log( + info( + `webchat listening on http://127.0.0.1:${webchatServer.port}/webchat/`, + ), + ); + } + await monitorWebProvider( Boolean(opts.verbose), undefined, @@ -748,7 +762,6 @@ Shows token usage per session when the agent reports it; set inbound.reply.agent const server = await startWebChatServer(port); const payload = { port: server.port, - token: server.token ?? null, basePath: "/webchat/", host: "127.0.0.1", }; diff --git a/src/config/config.ts b/src/config/config.ts index e77294ad3..1ba08031d 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -44,6 +44,11 @@ export type WebConfig = { reconnect?: WebReconnectConfig; }; +export type WebChatConfig = { + enabled?: boolean; + port?: number; +}; + export type TelegramConfig = { botToken?: string; requireMention?: boolean; @@ -101,6 +106,7 @@ export type WarelayConfig = { }; web?: WebConfig; telegram?: TelegramConfig; + webchat?: WebChatConfig; }; // New branding path (preferred) @@ -226,6 +232,12 @@ const WarelaySchema = z.object({ .optional(), }) .optional(), + webchat: z + .object({ + enabled: z.boolean().optional(), + port: z.number().int().positive().optional(), + }) + .optional(), telegram: z .object({ botToken: z.string().optional(), diff --git a/src/webchat/server.ts b/src/webchat/server.ts index 50cdd6614..32ee934b2 100644 --- a/src/webchat/server.ts +++ b/src/webchat/server.ts @@ -20,7 +20,6 @@ const WEBCHAT_DEFAULT_PORT = 18788; type WebChatServerState = { server: http.Server; port: number; - token?: string; }; let state: WebChatServerState | null = null; @@ -119,7 +118,7 @@ function notFound(res: http.ServerResponse) { res.end("Not Found"); } -export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT, token?: string) { +export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT) { if (state) return state; const root = resolveWebRoot(); @@ -147,7 +146,6 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT, token?: st res.end( JSON.stringify({ port, - token: token ?? null, sessionKey, storePath, sessionId, @@ -202,11 +200,23 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT, token?: st server.listen(port, "127.0.0.1", () => resolve()); }); - state = { server, port, token }; + state = { server, port }; logDebug(info(`webchat server listening on 127.0.0.1:${port}`)); return state; } +export async function ensureWebChatServerFromConfig() { + const cfg = loadConfig(); + if (cfg.webchat?.enabled === false) return null; + const port = cfg.webchat?.port ?? WEBCHAT_DEFAULT_PORT; + try { + return await startWebChatServer(port); + } catch (err) { + logDebug(`webchat server failed to start: ${String(err)}`); + throw err; + } +} + export function getWebChatServer(): WebChatServerState | null { return state; }