Skip to content

Commit 69d3624

Browse files
committed
fix(bridge): use OS-assigned port for auto-started web servers
tryStartWebServer used findFreePort to walk from coordinatorPort+1 (19877), which has a TOCTOU race — it probes, closes the probe, then the caller binds. Between close and bind, a concurrent agent grabs the same port. This caused EADDRINUSE when parallel subagents all started web servers simultaneously. Port 0 eliminates the race: the OS atomically assigns a unique free port for each listener. The PWA discovery path (probe from 19877) is only used when the page is served from a non-local host (e.g. GitHub Pages). When served locally (the bridge-started case), the browser connects via location.host directly — the port number doesn't need to be predictable. Remove the coordinatorPort parameter from tryStartWebServer — it was only used to compute the scan base, which is no longer needed.
1 parent 6b44305 commit 69d3624

2 files changed

Lines changed: 12 additions & 9 deletions

File tree

src/bridges/pi/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export default function (pi: ExtensionAPI) {
146146
cwd: process.cwd(),
147147
pid: process.pid,
148148
});
149-
webHandle = await tryStartWebServer(webCtrl, store.coordinatorPort);
149+
webHandle = await tryStartWebServer(webCtrl);
150150
}
151151

152152
refreshStatus();

src/bridges/user/web/server.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import type {
2929
MeshMessage,
3030
MeshStatePatch,
3131
} from "../../../core/wire-protocol.js";
32-
import { findFreePort } from "./port-discovery.js";
3332

3433
const WEB_HOST = "127.0.0.1";
3534

@@ -101,17 +100,21 @@ export interface WebServerHandle {
101100
// ---------------------------------------------------------------------------
102101

103102
/**
104-
* Start the web UI server on an OS-assigned port.
105-
* Returns the server handle, or undefined if port discovery fails.
103+
* Start the web UI server on an OS-assigned free port.
104+
*
105+
* Uses port 0 (OS-assigned) to avoid TOCTOU races when multiple bridges
106+
* start web servers concurrently — the OS atomically allocates a unique
107+
* free port for each.
108+
*
109+
* The PWA discovery path (probe from 19877) is only used when the page is
110+
* served from a non-local host (e.g. GitHub Pages). When served locally
111+
* (the common case for bridge-started servers), the browser connects via
112+
* location.host directly — so the port number doesn't need to be predictable.
106113
*/
107114
export async function tryStartWebServer(
108115
controller?: ChatController,
109-
coordinatorPort?: number,
110116
): Promise<WebServerHandle | undefined> {
111-
const base = (coordinatorPort ?? 19876) + 1;
112-
const port = await findFreePort(base);
113-
if (port === undefined) return undefined;
114-
return createWebServer(port, controller);
117+
return createWebServer(0, controller);
115118
}
116119

117120
/**

0 commit comments

Comments
 (0)