UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

867 lines (736 loc) 30.6 kB
/** * ExecutorShim — local-executor conformance shim for DaemonSupervisor * * Wraps the existing DaemonSupervisor and exposes the executor contract v1 * surface: register-on-start handshake, REST routes (dispatch/status/HITL/ * pause/resume/abort), and the WS event-stream endpoint. * * This is `isolation:none` — the agent runs directly in the host process. * No sandboxing is performed; callers should understand the security model * before exposing this on non-loopback interfaces. * * Event translation (supervisor → executor vocabulary): * loop:queued → (held; emitted as mission.assigned when dispatch received) * loop:started → mission.started * supervisor task:progress (simulated via progress ticks) → mission.progress * loop:completed → mission.completed * loop:failed → mission.failed * (operator abort) → mission.aborted * HITL detector → mission.hitl_required * graceful shutdown → mission.suspended for all in-flight missions * * @issue #1181 * @see docs/contracts/executor.v1.md * @see schemas/executor-v1.json */ import { EventEmitter } from 'node:events'; import { createServer } from 'node:http'; import { randomUUID } from 'node:crypto'; import os from 'node:os'; import { resolve as resolvePath, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // ── HITL detector patterns (pluggable via addHitlPattern) ──────────────────── const DEFAULT_HITL_PATTERNS = [ /\(y\/N\)/i, /\(yes\/no\)/i, /:\s*$/, /\?\s*$/, /Continue\?\s*/i, /Press Enter/i, /Do you want/i, /Are you sure/i, /\[y\/n\]/i, ]; // ── Platform detection ─────────────────────────────────────────────────────── // Sync version for module initialisation function detectPlatformCapabilitySync() { const plat = os.platform(); const arch = os.arch(); const archMap = { x64: 'x64', arm64: 'arm64', arm: 'arm' }; const mappedArch = archMap[arch] ?? arch; // WSL heuristic: linux + WSL env var const isWsl = plat === 'linux' && ( !!process.env.WSL_DISTRO_NAME || !!process.env.WSLENV ); if (isWsl) return `platform:wsl/${mappedArch}`; if (plat === 'darwin') return `platform:darwin/${mappedArch}`; if (plat === 'linux') return `platform:linux/${mappedArch}`; if (plat === 'win32') return `platform:win32/${mappedArch}`; return `platform:${plat}/${mappedArch}`; } const PLATFORM_CAPABILITY = detectPlatformCapabilitySync(); // ── ExecutorShim ───────────────────────────────────────────────────────────── /** * ExecutorShim wraps a DaemonSupervisor instance and exposes the v1 executor * contract surface. One shim instance per local-executor serve process. * * @fires ExecutorShim#event:ws — when an event should be streamed over WS */ export class ExecutorShim extends EventEmitter { /** * @param {object} opts * @param {object} opts.supervisor DaemonSupervisor instance * @param {string} opts.executorId Stable UUID for this executor * @param {string} opts.name Human-readable name * @param {string} opts.version Software version string * @param {string} opts.restBase REST base URL as seen by aiwg-serve (e.g. http://127.0.0.1:8200) * @param {string} opts.wsBase WS base URL as seen by aiwg-serve (e.g. ws://127.0.0.1:8200) * @param {string} opts.aiwgServeUrl Registration target URL (e.g. http://127.0.0.1:7337) * @param {string[]} [opts.hitlPatterns] Additional HITL regex patterns */ constructor(opts) { super(); if (!opts.supervisor) throw new Error('ExecutorShim: opts.supervisor is required'); if (!opts.executorId) throw new Error('ExecutorShim: opts.executorId is required'); this.supervisor = opts.supervisor; this.executorId = opts.executorId; this.name = opts.name ?? 'aiwg-local-executor'; this.version = opts.version ?? '1.0.0'; this.restBase = opts.restBase; this.wsBase = opts.wsBase; this.aiwgServeUrl = opts.aiwgServeUrl; this.hitlPatterns = [...DEFAULT_HITL_PATTERNS]; if (opts.hitlPatterns) { for (const p of opts.hitlPatterns) { this.hitlPatterns.push(p instanceof RegExp ? p : new RegExp(p)); } } /** * Live WS connections indexed by executorId (there will normally be one, * but support multiple for reconnect races). * @type {Set<WebSocket>} */ this._wsClients = new Set(); /** * Tracks in-flight missions. * missionId → { state, loopId, createdAt, updatedAt, recentEvents, pendingHitl } * @type {Map<string, MissionState>} */ this._missions = new Map(); /** * Maps loopId → missionId (reverse lookup for supervisor events). * @type {Map<string, string>} */ this._loopToMission = new Map(); /** * Token issued by aiwg-serve on register. Stored for reuse. * @type {string|null} */ this._token = null; /** * Per-mission stdout buffer used for HITL pattern detection. * loopId → string (last N bytes) * @type {Map<string, string>} */ this._stdoutBuffers = new Map(); this._wireSupervisorEvents(); } // ── Public API ───────────────────────────────────────────────────────────── /** * Add a custom HITL pattern at runtime. * @param {RegExp|string} pattern */ addHitlPattern(pattern) { this.hitlPatterns.push(pattern instanceof RegExp ? pattern : new RegExp(pattern)); } /** * Register with aiwg-serve. Should be called once at startup. * Retries on failure with exponential back-off (capped at 30 s). * Resolves with the issued token. */ async register(retryCount = 0) { const payload = { executor_id: this.executorId, name: this.name, version: this.version, spec_version: '1.0.0', transport_endpoints: { rest: this.restBase, ws: this.wsBase, }, capabilities: [ 'isolation:none', 'runtime:claude-code', 'runtime:codex', PLATFORM_CAPABILITY, 'hitl', ], }; try { const res = await fetch(`${this.aiwgServeUrl}/api/v1/executors/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const text = await res.text(); throw new Error(`register HTTP ${res.status}: ${text}`); } const body = await res.json(); this._token = body.token; this.emit('registered', { executorId: this.executorId, token: this._token }); return this._token; } catch (err) { if (retryCount < 8) { const delay = Math.min(30_000, 250 * (2 ** retryCount)); await new Promise(r => setTimeout(r, delay)); return this.register(retryCount + 1); } throw err; } } /** * Deregister from aiwg-serve. Called on graceful shutdown. */ async deregister() { if (!this._token) return; try { await fetch(`${this.aiwgServeUrl}/api/v1/executors/${this.executorId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${this._token}` }, }); } catch { /* best-effort */ } } // ── Mission operations (called by REST handlers) ─────────────────────────── /** * Accept a dispatched mission, hand it to the supervisor. * @param {string} sessionId — from the URL path * @param {object} body — validated dispatch_payload * @returns {{ missionId, estimatedStart }} or throws */ dispatch(sessionId, body) { const { mission_id: missionId, objective, completion, metadata = {} } = body; if (this._missions.has(missionId)) { throw Object.assign(new Error(`Mission ${missionId} already exists`), { status: 409 }); } const loopId = missionId; // 1:1 mapping const now = new Date().toISOString(); this._missions.set(missionId, { missionId, loopId, sessionId, state: 'assigned', createdAt: now, updatedAt: now, recentEvents: [], pendingHitl: null, objective, completion, metadata, }); this._loopToMission.set(loopId, missionId); // Emit mission.assigned immediately this._emitMissionEvent(missionId, 'mission.assigned', { state: 'assigned' }); // Submit to supervisor (runs the loop) try { this.supervisor.submit({ loopId, prompt: `${objective}\n\nCompletion criteria: ${completion}`, priority: metadata?.priority ?? 0, metadata: { missionId, sessionId, ...metadata }, }); } catch (err) { // Supervisor rejected (queue full, budget, circuit-breaker) — fail fast this._missions.delete(missionId); this._loopToMission.delete(loopId); throw Object.assign(err, { status: 503 }); } return { missionId, executorId: this.executorId, status: 'assigned', estimatedStart: new Date(Date.now() + 100).toISOString(), }; } /** * Return a mission status snapshot. */ getMission(missionId) { return this._missions.get(missionId) ?? null; } /** * Handle an incoming HITL response from the REST endpoint. * @param {string} missionId * @param {{ hitl_id: string, response: string }} body */ submitHitlResponse(missionId, body) { const mission = this._missions.get(missionId); if (!mission) throw Object.assign(new Error('Mission not found'), { status: 404 }); const { hitl_id, response } = body; if (!mission.pendingHitl || mission.pendingHitl.hitl_id !== hitl_id) { throw Object.assign( new Error(`No pending HITL request with id ${hitl_id}`), { status: 422 } ); } // Clear the pending HITL mission.pendingHitl = null; mission.state = 'running'; mission.updatedAt = new Date().toISOString(); // Inject response into agent stdin if we have a pid/stdin handle this._injectHitlResponse(mission.loopId, response); // Emit hitl_responded over both EE and WS — local listeners (tests, // metrics) consume the EventEmitter stream; aiwg-serve consumes the // WS stream. Using _emitMissionEvent keeps both in lockstep. this._emitMissionEvent(missionId, 'mission.hitl_responded', { hitl_id, response, responded_at: new Date().toISOString(), }); return { missionId, hitl_id, status: 'forwarded' }; } /** * Pause a running mission. */ pauseMission(missionId) { const mission = this._missions.get(missionId); if (!mission) throw Object.assign(new Error('Mission not found'), { status: 404 }); if (['done', 'failed', 'aborted'].includes(mission.state)) { throw Object.assign(new Error('Mission is in a terminal state'), { status: 409 }); } mission.state = 'paused'; mission.updatedAt = new Date().toISOString(); // Send SIGSTOP to the process group to actually pause it try { this.supervisor.killProcessGroup(mission.loopId, 'SIGSTOP'); } catch { /* ignore */ } this._emitMissionEvent(missionId, 'mission.paused', { state: 'paused', reason: 'operator request' }); return { missionId, status: 'paused' }; } /** * Resume a paused mission. */ resumeMission(missionId) { const mission = this._missions.get(missionId); if (!mission) throw Object.assign(new Error('Mission not found'), { status: 404 }); if (mission.state !== 'paused') { throw Object.assign(new Error('Mission is not paused'), { status: 409 }); } mission.state = 'running'; mission.updatedAt = new Date().toISOString(); try { this.supervisor.killProcessGroup(mission.loopId, 'SIGCONT'); } catch { /* ignore */ } this._emitMissionEvent(missionId, 'mission.resumed', { state: 'running', resumed_from: 'paused' }); return { missionId, status: 'resumed' }; } /** * Abort a mission. */ abortMission(missionId) { const mission = this._missions.get(missionId); if (!mission) throw Object.assign(new Error('Mission not found'), { status: 404 }); if (['done', 'failed', 'aborted'].includes(mission.state)) { throw Object.assign(new Error('Mission is in a terminal state'), { status: 409 }); } this.supervisor.cancel(mission.loopId); mission.state = 'aborted'; mission.updatedAt = new Date().toISOString(); mission.completedAt = mission.updatedAt; this._emitMissionEvent(missionId, 'mission.aborted', { state: 'aborted', aborted_by: 'operator', reason: 'operator request', }); return { missionId, status: 'aborted' }; } // ── WS management ────────────────────────────────────────────────────────── /** * Register a live WS client. Called when the /ws/executors/:id upgrade * completes on the shim's own server. * @param {WebSocket} ws */ addWsClient(ws) { this._wsClients.add(ws); // Send executor.resync immediately const ownedIds = [...this._missions.values()] .filter(m => !['done', 'failed', 'aborted'].includes(m.state)) .map(m => m.missionId); const resync = this._makeEnvelope(null, 'executor.resync', { owned_mission_ids: ownedIds }); try { ws.send(JSON.stringify(resync)); } catch { /* ignore */ } ws.on('close', () => { this._wsClients.delete(ws); }); ws.on('error', () => { this._wsClients.delete(ws); }); // Handle inbound WS messages (e.g. mission.hitl_responded from aiwg-serve) ws.on('message', (data) => { try { const msg = JSON.parse(data.toString()); if (msg.event === 'mission.hitl_responded' && msg.mission_id) { const mission = this._missions.get(msg.mission_id); if (mission && msg.data?.hitl_id && msg.data?.response) { this.submitHitlResponse(msg.mission_id, { hitl_id: msg.data.hitl_id, response: msg.data.response, }); } } } catch { /* ignore malformed */ } }); } // ── Graceful shutdown ─────────────────────────────────────────────────────── /** * Graceful shutdown — emit mission.suspended for all in-flight missions, * deregister from aiwg-serve, then resolve. */ async shutdown() { // Suspend all non-terminal missions for (const [missionId, mission] of this._missions) { if (!['done', 'failed', 'aborted'].includes(mission.state)) { mission.state = 'suspended'; mission.updatedAt = new Date().toISOString(); this._emitMissionEvent(missionId, 'mission.suspended', { state: 'suspended', checkpoint_id: `checkpoint-${missionId}-${Date.now()}`, reason: 'executor_shutdown', }); } } // Close all WS clients for (const ws of this._wsClients) { try { ws.close(1001, 'executor shutting down'); } catch { /* ignore */ } } this._wsClients.clear(); // Deregister await this.deregister(); } // ── Private: event wiring ─────────────────────────────────────────────────── _wireSupervisorEvents() { // loop:started (supervisor started actual agent process) this.supervisor.on('loop:started', ({ loopId, taskId }) => { const missionId = this._loopToMission.get(loopId); if (!missionId) return; const mission = this._missions.get(missionId); if (!mission) return; mission.state = 'running'; mission.updatedAt = new Date().toISOString(); this._emitMissionEvent(missionId, 'mission.started', { state: 'running', agent_runtime: 'claude-code', pty_session_id: `pty-${missionId}`, }); // Emit an initial progress event this._emitMissionEvent(missionId, 'mission.progress', { phase: 'initialising', summary: 'Agent started', }); }); // loop:completed (supervisor reports exit 0) this.supervisor.on('loop:completed', ({ loopId }) => { const missionId = this._loopToMission.get(loopId); if (!missionId) return; const mission = this._missions.get(missionId); if (!mission) return; mission.state = 'done'; mission.updatedAt = new Date().toISOString(); mission.completedAt = mission.updatedAt; this._emitMissionEvent(missionId, 'mission.completed', { state: 'done', exit_code: 0, summary: 'Mission completed successfully', }); this._loopToMission.delete(loopId); }); // loop:failed (supervisor reports non-zero exit or error) this.supervisor.on('loop:failed', ({ loopId, error, permanent }) => { const missionId = this._loopToMission.get(loopId); if (!missionId) return; const mission = this._missions.get(missionId); if (!mission) return; mission.state = 'failed'; mission.updatedAt = new Date().toISOString(); mission.completedAt = mission.updatedAt; this._emitMissionEvent(missionId, 'mission.failed', { state: 'failed', reason: permanent ? 'restart_intensity_exceeded' : 'exit_nonzero', error: String(error), }); this._loopToMission.delete(loopId); }); // Also handle loop:recovered (restart in progress) as a progress event this.supervisor.on('loop:recovered', ({ loopId, error }) => { const missionId = this._loopToMission.get(loopId); if (!missionId) return; this._emitMissionEvent(missionId, 'mission.progress', { phase: 'recovering', summary: `Agent restarting: ${String(error)}`, }); }); } /** * Inspect a stdout chunk for HITL patterns. * Called externally when the supervisor/PTY has output to offer. * * @param {string} loopId * @param {string} chunk */ handleStdoutChunk(loopId, chunk) { const missionId = this._loopToMission.get(loopId); if (!missionId) return; const mission = this._missions.get(missionId); if (!mission || mission.state !== 'running') return; // Maintain a rolling buffer (last 1 KB) const existing = this._stdoutBuffers.get(loopId) ?? ''; const combined = (existing + chunk).slice(-1024); this._stdoutBuffers.set(loopId, combined); for (const pattern of this.hitlPatterns) { if (pattern.test(combined)) { // Avoid re-triggering while a HITL is already pending if (mission.pendingHitl) break; const hitlId = `hitl-${missionId}-${Date.now()}`; mission.pendingHitl = { hitl_id: hitlId }; mission.state = 'hitl-required'; mission.updatedAt = new Date().toISOString(); this._emitMissionEvent(missionId, 'mission.hitl_required', { hitl_id: hitlId, prompt: combined.trim().split('\n').pop() || combined.trim(), context: combined.trim(), }); // Clear buffer so we don't re-fire this._stdoutBuffers.set(loopId, ''); break; } } } // ── Private: HITL stdin injection ────────────────────────────────────────── _injectHitlResponse(loopId, response) { const entry = this.supervisor._running?.get(loopId); if (!entry) return; // If the underlying AgentSupervisor exposes a stdin write on the task, // use it. Otherwise the response is best-effort. const task = this.supervisor.agentSupervisor?.tasks?.get(entry.taskId); if (task?.stdin && typeof task.stdin.write === 'function') { try { task.stdin.write(response + '\n'); } catch { /* ignore */ } return; } // Fallback: emit a synthetic progress note so the operator knows we tried const missionId = this._loopToMission.get(loopId); if (missionId) { this._emitMissionEvent(missionId, 'mission.progress', { phase: 'hitl_response', summary: `HITL response submitted: ${response}`, }); } } // ── Private: event helpers ────────────────────────────────────────────────── _makeEnvelope(missionId, event, data) { const envelope = { event, executor_id: this.executorId, ts: new Date().toISOString(), }; if (missionId) envelope.mission_id = missionId; if (data !== undefined) envelope.data = data; return envelope; } _appendMissionEvent(missionId, envelope) { const mission = this._missions.get(missionId); if (!mission) return; mission.recentEvents.push(envelope); if (mission.recentEvents.length > 50) { mission.recentEvents = mission.recentEvents.slice(-50); } mission.updatedAt = envelope.ts; } _emitMissionEvent(missionId, eventType, data) { const envelope = this._makeEnvelope(missionId, eventType, data); if (missionId) this._appendMissionEvent(missionId, envelope); this._broadcastWs(envelope); this.emit('event', envelope); } _broadcastWs(envelope) { const payload = JSON.stringify(envelope); for (const ws of this._wsClients) { try { if (ws.readyState === 1 /* OPEN */) ws.send(payload); } catch { /* ignore dead connection */ } } } } // ── HTTP / WS server factory ───────────────────────────────────────────────── /** * Build and start the executor's own HTTP+WS server. * * Routes: * POST /api/v1/sessions/:id/dispatch — accept a mission * GET /api/v1/missions/:id — mission status * POST /api/v1/missions/:id/hitl_response — HITL response * POST /api/v1/missions/:id/pause — pause * POST /api/v1/missions/:id/resume — resume * POST /api/v1/missions/:id/abort — abort * WS /ws/executors/:executor_id?token=<token> — event stream * * @param {ExecutorShim} shim * @param {{ port: number, bind: string, token: string }} opts * @returns {{ close: function, url: string }} */ export async function startExecutorServer(shim, opts) { const { port, bind, token } = opts; // ── Dynamic imports for optional deps ────────────────────────────────────── // eslint-disable-next-line no-new-func let honoMod, nodeMod; try { honoMod = await (new Function('m', 'return import(m)'))('hono'); nodeMod = await (new Function('m', 'return import(m)'))('@hono/node-server'); } catch { throw new Error( 'local-executor requires hono + @hono/node-server. ' + 'Install with: npm install --save-optional hono @hono/node-server' ); } const Hono = honoMod.Hono ?? honoMod.default?.Hono; const serve = nodeMod.serve ?? nodeMod.default?.serve; const getNodeListener = nodeMod.getNodeListener ?? nodeMod.default?.getNodeListener; if (!Hono || (!serve && !getNodeListener)) { throw new Error('Could not resolve Hono or serve from installed packages'); } // ── Token auth middleware ────────────────────────────────────────────────── function checkBearer(req) { const auth = req.header('Authorization') ?? ''; const t = auth.startsWith('Bearer ') ? auth.slice(7) : null; return t === token; } // ── Hono app ─────────────────────────────────────────────────────────────── const app = new Hono(); // Dispatch a mission app.post('/api/v1/sessions/:sessionId/dispatch', async (c) => { if (!checkBearer(c.req)) return c.json({ error: 'Unauthorized' }, 401); let body; try { body = await c.req.json(); } catch { return c.json({ error: 'Invalid JSON' }, 400); } const { mission_id, objective, completion } = body; if (!mission_id || !objective || !completion) { return c.json({ error: 'mission_id, objective, and completion are required' }, 400); } try { const result = shim.dispatch(c.req.param('sessionId'), body); return c.json(result, 202); } catch (err) { return c.json({ error: err.message }, err.status ?? 500); } }); // Mission status app.get('/api/v1/missions/:missionId', (c) => { if (!checkBearer(c.req)) return c.json({ error: 'Unauthorized' }, 401); const mission = shim.getMission(c.req.param('missionId')); if (!mission) return c.json({ error: 'Mission not found' }, 404); return c.json({ mission_id: mission.missionId, executor_id: shim.executorId, state: mission.state, created_at: mission.createdAt, updated_at: mission.updatedAt, completed_at: mission.completedAt, recent_events: mission.recentEvents.slice(-50), exit_code: mission.exitCode, error: mission.error, }); }); // HITL response app.post('/api/v1/missions/:missionId/hitl_response', async (c) => { if (!checkBearer(c.req)) return c.json({ error: 'Unauthorized' }, 401); let body; try { body = await c.req.json(); } catch { return c.json({ error: 'Invalid JSON' }, 400); } try { const result = shim.submitHitlResponse(c.req.param('missionId'), body); return c.json(result, 202); } catch (err) { return c.json({ error: err.message }, err.status ?? 500); } }); // Pause / resume / abort app.post('/api/v1/missions/:missionId/pause', (c) => { if (!checkBearer(c.req)) return c.json({ error: 'Unauthorized' }, 401); try { return c.json(shim.pauseMission(c.req.param('missionId')), 202); } catch (err) { return c.json({ error: err.message }, err.status ?? 500); } }); app.post('/api/v1/missions/:missionId/resume', (c) => { if (!checkBearer(c.req)) return c.json({ error: 'Unauthorized' }, 401); try { return c.json(shim.resumeMission(c.req.param('missionId')), 202); } catch (err) { return c.json({ error: err.message }, err.status ?? 500); } }); app.post('/api/v1/missions/:missionId/abort', (c) => { if (!checkBearer(c.req)) return c.json({ error: 'Unauthorized' }, 401); try { return c.json(shim.abortMission(c.req.param('missionId')), 202); } catch (err) { return c.json({ error: err.message }, err.status ?? 500); } }); // ── Node HTTP server with WS upgrade ────────────────────────────────────── // Build node listener from Hono app let nodeListener; if (getNodeListener) { nodeListener = getNodeListener(app.fetch); } else { // Older @hono/node-server: serve returns a server object nodeListener = app.fetch; } // Attempt to get a ws package let wsMod; try { wsMod = await (new Function('m', 'return import(m)'))('ws'); } catch { wsMod = null; } const WebSocketServer = wsMod?.WebSocketServer ?? wsMod?.default?.WebSocketServer ?? wsMod?.Server ?? wsMod?.default?.Server; let httpServer; if (serve && typeof serve === 'function') { // @hono/node-server ≥1.x: serve() returns the Node http.Server httpServer = serve({ fetch: app.fetch, port, hostname: bind, }); // serve() may return the server synchronously or as a promise if (httpServer && typeof httpServer.then === 'function') { httpServer = await httpServer; } } // Fallback: create our own Node HTTP server if (!httpServer || typeof httpServer.on !== 'function') { httpServer = createServer((req, res) => { const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`); const honoReq = new Request(`http://${req.headers.host}${req.url}`, { method: req.method, headers: Object.fromEntries( Object.entries(req.headers).filter(([, v]) => v !== undefined) .map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v]) ), }); app.fetch(honoReq).then(async (honoRes) => { res.writeHead(honoRes.status, Object.fromEntries(honoRes.headers)); const buf = await honoRes.arrayBuffer(); res.end(Buffer.from(buf)); }).catch(() => { res.writeHead(500); res.end('Internal server error'); }); }); httpServer.listen(port, bind); } // Wire WS upgrade for /ws/executors/:executorId if (WebSocketServer && httpServer && typeof httpServer.on === 'function') { const wss = new WebSocketServer({ noServer: true }); httpServer.on('upgrade', (req, socket, head) => { const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`); const match = url.pathname.match(/^\/ws\/executors\/([^/]+)$/); if (!match) { socket.destroy(); return; } const executorIdParam = match[1]; const tkParam = url.searchParams.get('token') ?? ''; if (executorIdParam !== shim.executorId || tkParam !== token) { socket.destroy(); return; } wss.handleUpgrade(req, socket, head, (ws) => { shim.addWsClient(ws); }); }); } const url = `http://${bind}:${port}`; return { url, close: () => { if (httpServer && typeof httpServer.close === 'function') { httpServer.close(); } }, }; }