diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index 71ce99b77..6cee06542 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -34,7 +34,9 @@ describe("canvas host", () => { }); try { - const res = await fetch(`http://127.0.0.1:${server.port}/`); + const res = await fetch( + `http://127.0.0.1:${server.port}${CANVAS_HOST_PATH}/`, + ); const html = await res.text(); expect(res.status).toBe(200); expect(html).toContain("Interactive test page"); @@ -111,7 +113,9 @@ describe("canvas host", () => { }); try { - const res = await fetch(`http://127.0.0.1:${server.port}/`); + const res = await fetch( + `http://127.0.0.1:${server.port}${CANVAS_HOST_PATH}/`, + ); const html = await res.text(); expect(res.status).toBe(200); expect(html).toContain("v1"); diff --git a/src/canvas-host/server.ts b/src/canvas-host/server.ts index 4a8dac7b0..f8f43b85a 100644 --- a/src/canvas-host/server.ts +++ b/src/canvas-host/server.ts @@ -377,7 +377,7 @@ export async function startCanvasHost( const handler = await createCanvasHostHandler({ runtime: opts.runtime, rootDir: opts.rootDir, - basePath: "/", + basePath: CANVAS_HOST_PATH, allowInTests: opts.allowInTests, }); diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index fc27c6e1a..56298adc8 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -1584,7 +1584,7 @@ describe("gateway server", () => { await new Promise((resolve) => ws.once("open", resolve)); const hello = await connectOk(ws, { token: "secret" }); - expect(hello.canvasHostUrl).toBe(`http://100.64.0.1:${port}`); + expect(hello.canvasHostUrl).toBe(`http://100.64.0.1:18793`); ws.close(); await server.close(); diff --git a/src/gateway/server.ts b/src/gateway/server.ts index e21b694be..17a0c9e87 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -24,7 +24,9 @@ import { } from "../canvas-host/a2ui.js"; import { type CanvasHostHandler, + type CanvasHostServer, createCanvasHostHandler, + startCanvasHost, } from "../canvas-host/server.js"; import { createDefaultDeps } from "../cli/deps.js"; import { agentCommand } from "../commands/agent.js"; @@ -395,6 +397,7 @@ const MAX_BUFFERED_BYTES = 1.5 * 1024 * 1024; // per-connection send buffer limi function deriveCanvasHostUrl( req: IncomingMessage | undefined, canvasPort: number | undefined, + hostOverride?: string, ) { if (!req || !canvasPort) return undefined; const hostHeader = req.headers.host?.trim(); @@ -406,8 +409,9 @@ function deriveCanvasHostUrl( : undefined; const scheme = forwardedProto === "https" ? "https" : "http"; - let host = ""; - if (hostHeader) { + let host = (hostOverride ?? "").trim(); + if (host === "0.0.0.0" || host === "::") host = ""; + if (!host && hostHeader) { try { const parsed = new URL(`http://${hostHeader}`); host = parsed.hostname; @@ -1025,6 +1029,7 @@ export async function startGatewayServer( } let canvasHost: CanvasHostHandler | null = null; + let canvasHostServer: CanvasHostServer | null = null; if (canvasHostEnabled) { try { const handler = await createCanvasHostHandler({ @@ -1318,6 +1323,31 @@ export async function startGatewayServer( return "0.0.0.0"; })(); + const canvasHostPort = (() => { + const configured = cfgAtStart.canvasHost?.port; + if (typeof configured === "number" && configured > 0) return configured; + return 18793; + })(); + + if (canvasHostEnabled && bridgeEnabled && bridgeHost) { + try { + const started = await startCanvasHost({ + runtime: defaultRuntime, + rootDir: cfgAtStart.canvasHost?.root, + port: canvasHostPort, + listenHost: bridgeHost, + allowInTests: opts.allowCanvasHostInTests, + }); + if (started.port > 0) { + canvasHostServer = started; + } + } catch (err) { + logWarn( + `gateway: canvas host failed to start on ${bridgeHost}:${canvasHostPort}: ${String(err)}`, + ); + } + } + const bridgeSubscribe = (nodeId: string, sessionKey: string) => { const normalizedNodeId = nodeId.trim(); const normalizedSessionKey = sessionKey.trim(); @@ -2079,11 +2109,7 @@ export async function startGatewayServer( }; const machineDisplayName = await getMachineDisplayName(); - const bridgeHostIsLoopback = bridgeHost ? isLoopbackHost(bridgeHost) : false; - const canvasHostPortForBridge = - canvasHost && (!isLoopbackHost(bindHost) || bridgeHostIsLoopback) - ? port - : undefined; + const canvasHostPortForBridge = canvasHostServer?.port; if (bridgeEnabled && bridgePort > 0 && bridgeHost) { try { @@ -2381,9 +2407,15 @@ export async function startGatewayServer( const remoteAddr = ( socket as WebSocket & { _socket?: { remoteAddress?: string } } )._socket?.remoteAddress; + const canvasHostPortForWs = canvasHostServer?.port ?? (canvasHost ? port : undefined); + const canvasHostOverride = + bridgeHost && bridgeHost !== "0.0.0.0" && bridgeHost !== "::" + ? bridgeHost + : undefined; const canvasHostUrl = deriveCanvasHostUrl( req, - canvasHost ? port : undefined, + canvasHostPortForWs, + canvasHostServer ? canvasHostOverride : undefined, ); logWs("in", "open", { connId, remoteAddr }); const isWebchatConnect = (params: ConnectParams | null | undefined) => @@ -4398,6 +4430,13 @@ export async function startGatewayServer( /* ignore */ } } + if (canvasHostServer) { + try { + await canvasHostServer.close(); + } catch { + /* ignore */ + } + } if (bridge) { try { await bridge.close();