refactor(serve): WS client calls app.fetch directly, no HTTP server in gateway mode

- WS client receives app.fetch function instead of localPort
- Gateway mode no longer starts a local HTTP server
- Local-only mode (no secret) still starts HTTP server as before
- Removes unnecessary HTTP round-trip for gateway requests
This commit is contained in:
2026-05-13 22:50:49 +08:00
parent e14643a50b
commit 0ffd84cf7d
2 changed files with 15 additions and 20 deletions
@@ -16,9 +16,8 @@ const HEARTBEAT_INTERVAL_MS = 60_000;
export function startServer(
storageRoot: string,
options: ServeOptions,
agentToken: string | null,
): void {
const app = createApp(storageRoot, agentToken);
const app = createApp(storageRoot, null);
const server = serve({
fetch: app.fetch,
@@ -95,33 +94,32 @@ export async function dispatchServe(storageRoot: string, argv: string[]): Promis
if (options.gatewaySecret === "") {
// No gateway — local-only mode
startServer(storageRoot, options, null);
startServer(storageRoot, options);
printCliLine("no WORKFLOW_GATEWAY_SECRET — running in local-only mode");
await new Promise(() => {});
return 0;
}
// Gateway mode — no HTTP server, WS client calls app.fetch directly
const agentToken = randomUUID();
startServer(storageRoot, options, agentToken);
const app = createApp(storageRoot, agentToken);
// Start WebSocket reverse connection to gateway
const log = createLogger({ sink: { kind: "stderr" } });
const stopWsClient = startGatewayWsClient({
gatewayUrl: options.gatewayUrl,
name: options.name,
secret: options.gatewaySecret,
localPort: options.port,
appFetch: app.fetch,
log,
});
printCliLine("connected to gateway via WebSocket");
printCliLine("connected to gateway via WebSocket (no local HTTP server)");
// Register with gateway for discovery
const localUrl = `http://127.0.0.1:${options.port}`;
const registered = await registerWithGateway(
options.gatewayUrl,
options.name,
localUrl,
`ws://${options.name}`,
options.gatewaySecret,
agentToken,
);
@@ -132,7 +130,7 @@ export async function dispatchServe(storageRoot: string, argv: string[]): Promis
const heartbeatTimer = startHeartbeat(
options.gatewayUrl,
options.name,
localUrl,
`ws://${options.name}`,
options.gatewaySecret,
agentToken,
HEARTBEAT_INTERVAL_MS,
@@ -5,7 +5,7 @@ export type GatewayWsClientParams = {
gatewayUrl: string;
name: string;
secret: string;
localPort: number;
appFetch: (request: Request) => Response | Promise<Response>;
log: LogFn;
};
@@ -44,20 +44,17 @@ async function handleGatewayMessage(
params.log("ZM8K2PQ1", "gateway WebSocket dropped non-request message");
return;
}
const localUrl = `http://127.0.0.1:${String(params.localPort)}${req.path}`;
const initHeaders = new Headers();
for (const [k, v] of Object.entries(req.headers)) {
initHeaders.set(k, v);
}
const localUrl = `http://localhost${req.path}`;
const headers = new Headers(req.headers);
let resp: Response;
try {
resp = await fetch(localUrl, {
resp = await params.appFetch(new Request(localUrl, {
method: req.method,
headers: initHeaders,
headers,
body: req.body === null ? undefined : req.body,
});
}));
} catch (e) {
params.log("R4N7BQ3C", `local proxy fetch failed: ${String(e)}`);
params.log("R4N7BQ3C", `app.fetch failed: ${String(e)}`);
const errBody: WsResponse = {
id: req.id,
status: 502,