@hua-labs/tap 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,12 @@
1
1
  # @hua-labs/tap
2
2
 
3
- > *Other tools give agents instructions. tap gives them context.*
4
-
5
- **탑 (塔)** — Korean for *stone tower* and *control tower*. Stone towers are built by stacking stones one by one. Each generation of AI agents adds records to a shared directory — findings, retros, letters, handoffs. The tower grows. A control tower observes and coordinates. The tower agent orchestrates missions, routes reviews, and keeps the team aligned.
6
-
7
- *"돌이 쌓이면 탑이 된다"* — When stones stack, they become a tower.
8
-
9
3
  Zero-dependency CLI for cross-model AI agent communication setup.
10
4
 
11
5
  One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
12
6
 
13
- ### Why "tap"?
14
-
15
- 탑 (塔) — Korean for **stone tower** and **control tower**.
16
-
17
- - **Stone tower** (석탑): built by stacking stones one by one. Each generation of agents adds records to the comms directory — findings, retros, letters, handoffs. The tower grows.
18
- - **Control tower** (관제탑): observes and coordinates from the center. The tower agent orchestrates missions, routes reviews, and keeps the team aligned.
19
-
20
- *Stacked records + central coordination = tap.*
21
-
22
7
  ## Quick Start
23
8
 
24
- > `bun` is required to run the managed tap MCP server. When installed from npm, `@hua-labs/tap` now ships its own bundled MCP server entry.
9
+ > `npx @hua-labs/tap` ships a bundled managed MCP server entry and runs that bundled `.mjs` with `node`. `bun` is only required when tap falls back to repo-local TypeScript sources during monorepo or local-dev workflows.
25
10
 
26
11
  ```bash
27
12
  # 1. Initialize comms directory and state
@@ -90,49 +75,23 @@ Output shows three status levels:
90
75
 
91
76
  ### `doctor`
92
77
 
93
- Diagnose and optionally fix tap infrastructure health.
78
+ Diagnose config drift, bridge health, managed MCP wiring, and runtime state. Use `--fix` to repair common config drift, including Codex `approval_mode` mismatches.
94
79
 
95
80
  ```bash
96
81
  npx @hua-labs/tap doctor
97
82
  npx @hua-labs/tap doctor --fix
98
83
  ```
99
84
 
100
- ### `up` / `down`
101
-
102
- Start or stop all managed bridges.
103
-
104
- ```bash
105
- npx @hua-labs/tap up
106
- npx @hua-labs/tap down
107
- ```
108
-
109
- ### `gui`
110
-
111
- Start a local web dashboard showing bridge status, agents, mission kanban, and PR board.
112
-
113
- ```bash
114
- npx @hua-labs/tap gui
115
- ```
116
-
117
- ### `watch`
118
-
119
- Autonomous bridge health monitoring with auto-restart for stuck bridges.
120
-
121
- ```bash
122
- npx @hua-labs/tap watch
123
- npx @hua-labs/tap watch --loop --interval 60
124
- ```
125
-
126
85
  ### `serve`
127
86
 
128
- Start the tap MCP server (stdio). Convenience command for running the MCP server locally.
87
+ Start the tap-comms MCP server (stdio). Convenience command for running the MCP server locally.
129
88
 
130
89
  ```bash
131
90
  npx @hua-labs/tap serve
132
91
  npx @hua-labs/tap serve --comms-dir /path/to/comms
133
92
  ```
134
93
 
135
- Requires `bun`. Uses the bundled MCP server entry from `@hua-labs/tap`, with a repo-local fallback for monorepo checkouts.
94
+ For npm installs, `serve` runs the bundled `mcp-server.mjs` entry with `node`. In monorepos or local checkouts, tap may fall back to repo-local `.ts` sources, which still require `bun`.
136
95
 
137
96
  ## Supported Runtimes
138
97
 
@@ -158,9 +117,9 @@ npx @hua-labs/tap status --json
158
117
  "message": "2 runtime(s) installed",
159
118
  "warnings": [],
160
119
  "data": {
161
- "version": "0.3.0",
120
+ "version": "0.x.y",
162
121
  "commsDir": "/path/to/comms",
163
- "instances": {
122
+ "runtimes": {
164
123
  "claude": { "status": "active", "bridgeMode": "native-push" },
165
124
  "codex": { "status": "configured", "bridgeMode": "app-server" }
166
125
  }
@@ -215,56 +174,46 @@ Each runtime has an adapter that:
215
174
 
216
175
  The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
217
176
 
218
- ## What's New (0.3.0)
219
-
220
- ### Headless Durable
177
+ ## Recent Changes
221
178
 
222
- TUI-free Codex operation is now fully automated:
223
- - **Auto app-server spawn** — `tap bridge start` launches codex app-server without manual setup
224
- - **Thread self-heal** — Stale thread state automatically reconciled from heartbeat
225
- - **Warmup on restart** — Cold-start warmup triggers on `bridge restart`, not just `tap up`
179
+ ### Config And Lifecycle
226
180
 
227
- ### Web Dashboard
181
+ - **Layered config resolution** — ConfigSource-based loading, instance config isolation, and runtime drift detection reduce cross-instance config bleed-through
182
+ - **Managed lifecycle** — server lifecycle state, dual-session prevention, and health monitoring make bridge startup and recovery more predictable
183
+ - **Repair path** — `tap doctor --fix` can now repair more managed config drift, including Codex MCP table mismatches
228
184
 
229
- ```bash
230
- npx @hua-labs/tap gui
231
- ```
232
-
233
- Live dashboard at `http://127.0.0.1:3847` with:
234
- - Agent status + bridge health (SSE live updates)
235
- - Mission kanban board (`/missions`)
236
- - PR board (`/prs`)
237
- - JSON APIs with CORS (`/api/snapshot`, `/api/missions`, `/api/prs`)
238
-
239
- ### Autonomous Monitoring
240
-
241
- ```bash
242
- npx @hua-labs/tap watch --loop --interval 60
243
- ```
185
+ ### Identity And Routing
244
186
 
245
- Continuous health monitoring with auto-restart for stuck bridges. Cron/systemd friendly.
187
+ - **Permission mode + routing** — permission mode support, qualified name routing, and the name-claim protocol tighten runtime identity semantics
188
+ - **Claim safety** — same-instance claim stealing is blocked while a live claim is still valid, while expired claims can still be reclaimed safely
246
189
 
247
- ### Cross-Platform
190
+ ### Bridge And Runtime Updates
248
191
 
249
- - **Windows**: PowerShell hidden spawn + `.cmd` shim unwrap
250
- - **macOS/Linux**: Unix detached process + `lsof` PID discovery
251
- - **Gemini**: Fake IDE companion server (MCP-over-HTTP)
192
+ - **Bridge split and cleanup** the legacy `bridge.ts` monolith was split into focused modules, then the old wrapper logic was removed
193
+ - **Codex MCP defaults** managed Codex installs now persist `[mcp_servers.tap] approval_mode = "auto"` and re-sync the runtime config hash when tap rewrites managed config
194
+ - **Bundled MCP runtime** bundled `.mjs` server entries now prefer `node`; repo-local TypeScript sources still use `bun`
195
+ - **Hotfixes** — ESM `require()` breakage, temp file leaks in name claims, and claim-stealing edge cases were fixed during publish prep
252
196
 
253
- ### Modular Architecture
197
+ ### Trust Layer And Delivery
254
198
 
255
- bridge.ts split from 1,744 to 241 lines (-86%) across 16 focused modules. See `docs/areas/tap/splitting-convention.md`.
199
+ - **Shared vs runtime state split** `TAP_STATE_DIR` remains the shared source of truth while `TAP_RUNTIME_STATE_DIR` is reserved for per-bridge runtime files, so headless restarts and later TUI attaches keep the same identity contract
200
+ - **Attached TUI rebind** — Codex TUI attach can now recover `agentId` and `agentName` from runtime heartbeat and agent-name files without relying on per-session env injection
201
+ - **State surface alignment** — bridge status, runtime heartbeat, and presence now read from the same state surfaces, reducing mismatches between `tap status`, bridge state, and plugin-visible presence
202
+ - **Broadcast dedupe** — bridge-dispatched notifications are deduplicated so one broadcast does not fan out twice
203
+ - **Ack storm prevention** — peer DM auto-replies are rate-limited to stop acknowledgement loops from flooding the inbox
256
204
 
257
- ## Examples
205
+ ### Test Hardening
258
206
 
259
- Real multi-agent collaboration highlights from 18 generations:
207
+ - **CLI-path coverage** integration tests now exercise the actual `bridge` and `up` command paths that patch Codex `approval_mode`
208
+ - **Publish prep stabilization** — failing suites were fixed or quarantined so release-blocking regressions show up earlier in the main package tests
260
209
 
261
- - [Logic Battle: "Will You Ship Broken Code?"](examples/01-logic-battle-known-broken.md)
262
- - [Cross-Model Review Catches Root Cause Misdiagnosis](examples/02-cross-model-review-root-cause.md)
263
- - [Independent Convergence Across 3 Generations](examples/03-convergence-pattern.md)
264
- - [Tower Broadcast: "Stop Talking, Write Code"](examples/04-tower-broadcast.md)
265
- - [Self-Awareness ≠ Self-Correction](examples/05-self-awareness-paradox.md)
210
+ ## Migration Notes
266
211
 
267
- [See all 10 examples →](examples/)
212
+ - **No hard breaking API change is intended in this release train**, but managed runtime defaults changed. Treat this as an operational migration, especially for Codex setups.
213
+ - **Bundled MCP command changed for packaged installs** — if your managed `config.toml` still points bundled tap MCP entries at `bun`, rerun `npx @hua-labs/tap add codex --force` or `npx @hua-labs/tap doctor --fix` so bundled `.mjs` entries switch to `node`.
214
+ - **Repo-local source workflows still use `bun`** — local monorepo or source-checkout paths can still resolve to `.ts` server entries, so keep `bun` installed for development workflows.
215
+ - **Codex approval mode should be `auto`** — managed Codex installs are expected to end up with `[mcp_servers.tap] approval_mode = "auto"`. `tap doctor --fix` will repair stale managed tables.
216
+ - **Restart Codex bridges after upgrading** — managed bridge launches now export both `TAP_STATE_DIR` and `TAP_RUNTIME_STATE_DIR`; restart existing bridge processes so headless/runtime identity repair is active end-to-end.
268
217
 
269
218
  ## License
270
219
 
@@ -223,13 +223,23 @@ function writeNotFound(response) {
223
223
  response.end("Not Found");
224
224
  }
225
225
  function rejectUpgrade(socket, statusCode) {
226
- socket.write(`HTTP/1.1 ${statusCode} ${statusCode === 404 ? "Not Found" : "Bad Request"}\r
226
+ socket.write(
227
+ `HTTP/1.1 ${statusCode} ${statusCode === 404 ? "Not Found" : "Bad Request"}\r
227
228
  \r
228
- `);
229
+ `
230
+ );
229
231
  socket.destroy();
230
232
  }
233
+ function containsTraversal(raw) {
234
+ if (raw.includes("..")) return true;
235
+ if (/%2e/i.test(raw) && raw.replace(/%2e/gi, ".").includes("..")) return true;
236
+ return false;
237
+ }
231
238
  function isUpgradePath(listenUrl, request) {
232
- const requestUrl = new URL(request.url ?? "/", listenUrl.replace(/^ws/, "http"));
239
+ const requestUrl = new URL(
240
+ request.url ?? "/",
241
+ listenUrl.replace(/^ws/, "http")
242
+ );
233
243
  const listenPath = new URL(listenUrl).pathname;
234
244
  return requestUrl.pathname === (listenPath || "/");
235
245
  }
@@ -294,22 +304,31 @@ async function startGatewayServer(options) {
294
304
  closeSocket(upstream, 1011, "Client error");
295
305
  });
296
306
  });
307
+ const listenPath = new URL(options.listenUrl).pathname || "/";
297
308
  const server = createServer(async (request, response) => {
298
309
  const requestUrl = new URL(
299
310
  request.url ?? "/",
300
311
  options.listenUrl.replace(/^ws/, "http")
301
312
  );
313
+ if (containsTraversal(request.url ?? "")) {
314
+ writeNotFound(response);
315
+ return;
316
+ }
302
317
  if (request.method === "GET" && requestUrl.pathname === GATEWAY_READYZ_PATH) {
303
318
  await handleReadyzRequest(response, options);
304
319
  return;
305
320
  }
306
- if (isUpgradePath(options.listenUrl, request)) {
321
+ if (requestUrl.pathname === listenPath) {
307
322
  writeUpgradeRequired(response);
308
323
  return;
309
324
  }
310
325
  writeNotFound(response);
311
326
  });
312
327
  server.on("upgrade", (request, socket, head) => {
328
+ if (containsTraversal(request.url ?? "")) {
329
+ rejectUpgrade(socket, 400);
330
+ return;
331
+ }
313
332
  if (!isUpgradePath(options.listenUrl, request)) {
314
333
  rejectUpgrade(socket, 404);
315
334
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bridges/codex-app-server-auth-gateway.ts","../../src/engine/bridge-app-server-health.ts"],"sourcesContent":["import {\n createServer,\n type IncomingMessage,\n type Server as HttpServer,\n type ServerResponse,\n} from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport type { Socket } from \"node:net\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport { WebSocket, WebSocketServer, type RawData } from \"ws\";\nimport { checkManagedAppServerReady } from \"../engine/bridge-app-server-health.js\";\n\nconst AUTH_SUBPROTOCOL_PREFIX = \"tap-auth-\";\nconst CLOSE_UNAUTHORIZED = 4401;\nconst CLOSE_UPSTREAM_ERROR = 1013;\nexport const GATEWAY_READYZ_PATH = \"/readyz\";\n\nexport interface GatewayOptions {\n listenUrl: string;\n upstreamUrl: string;\n token: string;\n}\n\nexport interface GatewayRuntime {\n server: HttpServer;\n close(): Promise<void>;\n}\n\nfunction normalizeUrl(value: string): string {\n return value.replace(/\\/$/, \"\");\n}\n\nfunction closeSocket(\n socket: Pick<WebSocket, \"readyState\" | \"close\">,\n code: number,\n reason: string,\n): void {\n if (\n socket.readyState === WebSocket.CLOSING ||\n socket.readyState === WebSocket.CLOSED\n ) {\n return;\n }\n\n try {\n socket.close(code, reason);\n } catch {\n // Best-effort cleanup only.\n }\n}\n\nfunction readFlagValue(argv: string[], index: number, flag: string): string {\n const current = argv[index] ?? \"\";\n const eqIndex = current.indexOf(\"=\");\n if (eqIndex >= 0) {\n return current.slice(eqIndex + 1);\n }\n\n const next = argv[index + 1];\n if (!next || next.startsWith(\"--\")) {\n throw new Error(`Missing value for ${flag}`);\n }\n return next;\n}\n\nexport function buildGatewayOptions(argv: string[]): GatewayOptions {\n let listenUrl = process.env.TAP_GATEWAY_LISTEN_URL?.trim() || \"\";\n let upstreamUrl = process.env.TAP_GATEWAY_UPSTREAM_URL?.trim() || \"\";\n let tokenFile = process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || \"\";\n let token = process.env.TAP_GATEWAY_TOKEN?.trim() || \"\";\n\n for (let index = 0; index < argv.length; index += 1) {\n const flag = argv[index] ?? \"\";\n const consumesNext = !flag.includes(\"=\");\n\n if (flag.startsWith(\"--listen-url\")) {\n listenUrl = readFlagValue(argv, index, \"--listen-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--upstream-url\")) {\n upstreamUrl = readFlagValue(argv, index, \"--upstream-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token\")) {\n token = readFlagValue(argv, index, \"--token\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token-file\")) {\n tokenFile = readFlagValue(argv, index, \"--token-file\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n }\n\n if (tokenFile) {\n token = readFileSync(tokenFile, \"utf8\").trim();\n }\n\n if (!listenUrl) {\n throw new Error(\"Missing gateway listen URL\");\n }\n if (!upstreamUrl) {\n throw new Error(\"Missing gateway upstream URL\");\n }\n if (!token) {\n throw new Error(\"Missing gateway auth token\");\n }\n\n const listen = new URL(listenUrl);\n const upstream = new URL(upstreamUrl);\n if (!/^wss?:$/.test(listen.protocol)) {\n throw new Error(`Unsupported gateway listen protocol: ${listen.protocol}`);\n }\n if (!/^wss?:$/.test(upstream.protocol)) {\n throw new Error(\n `Unsupported gateway upstream protocol: ${upstream.protocol}`,\n );\n }\n\n return {\n listenUrl: normalizeUrl(listen.toString()),\n upstreamUrl: normalizeUrl(upstream.toString()),\n token,\n };\n}\n\nfunction tokensMatch(\n presentedToken: string | null,\n expectedToken: string,\n): boolean {\n if (!presentedToken) {\n return false;\n }\n\n const presented = Buffer.from(presentedToken, \"utf8\");\n const expected = Buffer.from(expectedToken, \"utf8\");\n if (presented.length !== expected.length) {\n return false;\n }\n\n return timingSafeEqual(presented, expected);\n}\n\nasync function main(): Promise<void> {\n const options = buildGatewayOptions(process.argv.slice(2));\n const runtime = await startGatewayServer(options);\n\n const shutdown = () => {\n void runtime.close().finally(() => {\n process.exit(0);\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nfunction writeJson(\n response: ServerResponse,\n statusCode: number,\n body: Record<string, unknown>,\n): void {\n response.statusCode = statusCode;\n response.setHeader(\"Content-Type\", \"application/json\");\n response.end(JSON.stringify(body));\n}\n\nfunction writeUpgradeRequired(response: ServerResponse): void {\n response.statusCode = 426;\n response.setHeader(\"Connection\", \"Upgrade\");\n response.setHeader(\"Upgrade\", \"websocket\");\n response.end(\"Upgrade Required\");\n}\n\nfunction writeNotFound(response: ServerResponse): void {\n response.statusCode = 404;\n response.end(\"Not Found\");\n}\n\nfunction rejectUpgrade(socket: Socket | import(\"stream\").Duplex, statusCode: number): void {\n socket.write(`HTTP/1.1 ${statusCode} ${statusCode === 404 ? \"Not Found\" : \"Bad Request\"}\\r\\n\\r\\n`);\n socket.destroy();\n}\n\nfunction isUpgradePath(listenUrl: string, request: IncomingMessage): boolean {\n const requestUrl = new URL(request.url ?? \"/\", listenUrl.replace(/^ws/, \"http\"));\n const listenPath = new URL(listenUrl).pathname;\n return requestUrl.pathname === (listenPath || \"/\");\n}\n\nasync function handleReadyzRequest(\n response: ServerResponse,\n options: GatewayOptions,\n): Promise<void> {\n const ready = await checkManagedAppServerReady(options.upstreamUrl);\n writeJson(response, ready ? 200 : 503, { ok: ready });\n}\n\nexport async function startGatewayServer(\n options: GatewayOptions,\n): Promise<GatewayRuntime> {\n const listen = new URL(options.listenUrl);\n const host = listen.hostname === \"localhost\" ? \"127.0.0.1\" : listen.hostname;\n const port = Number.parseInt(listen.port, 10);\n if (!Number.isFinite(port) || port <= 0) {\n throw new Error(\n `Gateway listen URL must include a valid port: ${options.listenUrl}`,\n );\n }\n\n const wsServer = new WebSocketServer({\n noServer: true,\n perMessageDeflate: false,\n });\n\n wsServer.on(\"connection\", (client: WebSocket, request: IncomingMessage) => {\n // Extract token from Sec-WebSocket-Protocol header (subprotocol auth).\n // Client sends: WebSocket(url, [\"tap-auth-<token>\"])\n // Falls back to query param for backward compatibility during migration.\n const protocols =\n request.headers[\"sec-websocket-protocol\"]\n ?.split(\",\")\n .map((s) => s.trim()) ?? [];\n const authProtocol = protocols.find((p) =>\n p.startsWith(AUTH_SUBPROTOCOL_PREFIX),\n );\n const subprotocolToken =\n authProtocol?.slice(AUTH_SUBPROTOCOL_PREFIX.length) ?? null;\n\n // Legacy fallback: query param (will be removed in future version)\n const requestUrl = new URL(request.url ?? \"/\", options.listenUrl);\n const queryToken = requestUrl.searchParams.get(\"tap_token\");\n\n const presentedToken = subprotocolToken ?? queryToken;\n if (!tokensMatch(presentedToken, options.token)) {\n closeSocket(client, CLOSE_UNAUTHORIZED, \"Unauthorized\");\n return;\n }\n\n const upstream = new WebSocket(options.upstreamUrl, {\n perMessageDeflate: false,\n });\n\n upstream.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n });\n\n client.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n }\n });\n\n upstream.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Upstream closed\";\n closeSocket(client, code || 1000, reason);\n });\n\n client.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Client closed\";\n closeSocket(upstream, code || 1000, reason);\n });\n\n upstream.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] upstream error: ${String(error)}`);\n closeSocket(client, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n closeSocket(upstream, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n });\n\n client.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] client error: ${String(error)}`);\n closeSocket(upstream, 1011, \"Client error\");\n });\n });\n\n const server = createServer(async (request, response) => {\n const requestUrl = new URL(\n request.url ?? \"/\",\n options.listenUrl.replace(/^ws/, \"http\"),\n );\n\n if (request.method === \"GET\" && requestUrl.pathname === GATEWAY_READYZ_PATH) {\n await handleReadyzRequest(response, options);\n return;\n }\n\n if (isUpgradePath(options.listenUrl, request)) {\n writeUpgradeRequired(response);\n return;\n }\n\n writeNotFound(response);\n });\n\n server.on(\"upgrade\", (request, socket, head) => {\n if (!isUpgradePath(options.listenUrl, request)) {\n rejectUpgrade(socket, 404);\n return;\n }\n\n wsServer.handleUpgrade(request, socket, head, (client) => {\n wsServer.emit(\"connection\", client, request);\n });\n });\n\n await new Promise<void>((resolvePromise, rejectPromise) => {\n server.once(\"error\", rejectPromise);\n server.listen(port, host, () => {\n server.off(\"error\", rejectPromise);\n console.log(\n `[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`,\n );\n resolvePromise();\n });\n });\n\n return {\n server,\n close() {\n return new Promise<void>((resolvePromise) => {\n server.close(() => {\n wsServer.close(() => resolvePromise());\n });\n });\n },\n };\n}\n\nfunction isDirectExecution(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n return import.meta.url === pathToFileURL(resolve(entry)).href;\n}\n\nif (isDirectExecution()) {\n main().catch((error) => {\n console.error(\n error instanceof Error ? (error.stack ?? error.message) : String(error),\n );\n process.exit(1);\n });\n}\n","import * as net from \"node:net\";\nimport type { AppServerState } from \"../types.js\";\nimport { getWebSocketCtor, delay } from \"./bridge-port-network.js\";\n\nexport interface WebSocketLike {\n addEventListener(\n type: \"open\" | \"error\" | \"close\",\n listener: () => void,\n options?: { once?: boolean },\n ): void;\n close(code?: number, reason?: string): void;\n}\n\nexport type WebSocketCtor = new (\n url: string,\n protocols?: string | string[],\n) => WebSocketLike;\n\nexport const APP_SERVER_HEALTH_TIMEOUT_MS = 1_500;\nexport const APP_SERVER_HEALTH_RETRY_MS = 250;\nexport const APP_SERVER_READYZ_PATH = \"/readyz\";\n\nexport const AUTH_SUBPROTOCOL_PREFIX = \"tap-auth-\";\n\nexport type AppServerReadyzStatus = \"ready\" | \"not-ready\" | \"unsupported\";\n\nexport async function checkAppServerHealth(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n gatewayToken?: string | null,\n): Promise<boolean> {\n const WebSocket = getWebSocketCtor();\n if (!WebSocket) {\n return false;\n }\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n let socket: WebSocketLike | null = null;\n\n const finish = (healthy: boolean) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n try {\n socket?.close();\n } catch {\n // Best-effort cleanup only.\n }\n resolve(healthy);\n };\n\n const timer = setTimeout(() => finish(false), timeoutMs);\n\n try {\n // Authenticate via WebSocket subprotocol when a gateway token is provided.\n const protocols = gatewayToken\n ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`]\n : undefined;\n socket = new WebSocket(url, protocols);\n socket.addEventListener(\"open\", () => finish(true), { once: true });\n socket.addEventListener(\"error\", () => finish(false), { once: true });\n socket.addEventListener(\"close\", () => finish(false), { once: true });\n } catch {\n finish(false);\n }\n });\n}\n\nexport async function waitForAppServerHealth(\n url: string,\n timeoutMs: number,\n gatewayToken?: string | null,\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n if (\n await checkAppServerHealth(\n url,\n APP_SERVER_HEALTH_TIMEOUT_MS,\n gatewayToken,\n )\n ) {\n return true;\n }\n await delay(APP_SERVER_HEALTH_RETRY_MS);\n }\n\n return false;\n}\n\nexport function buildAppServerReadyzUrl(url: string): string | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n\n if (parsed.protocol === \"ws:\") {\n parsed.protocol = \"http:\";\n } else if (parsed.protocol === \"wss:\") {\n parsed.protocol = \"https:\";\n } else if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return null;\n }\n\n parsed.pathname = APP_SERVER_READYZ_PATH;\n parsed.search = \"\";\n parsed.hash = \"\";\n return parsed.toString();\n}\n\nexport async function checkAppServerReadyz(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n): Promise<AppServerReadyzStatus> {\n const readyzUrl = buildAppServerReadyzUrl(url);\n if (!readyzUrl) {\n return \"unsupported\";\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(readyzUrl, {\n method: \"GET\",\n signal: controller.signal,\n headers: {\n accept: \"application/json\",\n },\n });\n\n if (response.ok) {\n return \"ready\";\n }\n\n if (\n response.status === 400 ||\n response.status === 404 ||\n response.status === 405 ||\n response.status === 426 ||\n response.status === 501\n ) {\n return \"unsupported\";\n }\n\n return \"not-ready\";\n } catch {\n return \"not-ready\";\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Check if a TCP port is accepting connections (without WebSocket upgrade).\n * Use this for managed startup health checks to avoid creating app-server sessions.\n */\nexport async function checkTcpPortListening(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n): Promise<boolean> {\n let hostname: string;\n let port: number;\n try {\n const parsed = new URL(url.replace(/^ws/, \"http\"));\n hostname = parsed.hostname;\n port = parseInt(parsed.port, 10);\n } catch {\n return false;\n }\n if (!port || !Number.isFinite(port)) return false;\n\n return new Promise<boolean>((resolve) => {\n const socket = net.createConnection({ host: hostname, port });\n const timer = setTimeout(() => {\n socket.destroy();\n resolve(false);\n }, timeoutMs);\n\n socket.once(\"connect\", () => {\n clearTimeout(timer);\n socket.destroy();\n resolve(true);\n });\n socket.once(\"error\", () => {\n clearTimeout(timer);\n socket.destroy();\n resolve(false);\n });\n });\n}\n\n/**\n * Wait for a TCP port to start accepting connections.\n * Does NOT open a WebSocket, so no app-server session is created.\n */\nexport async function waitForTcpPortListening(\n url: string,\n timeoutMs: number,\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n if (await checkTcpPortListening(url, APP_SERVER_HEALTH_TIMEOUT_MS)) {\n return true;\n }\n await delay(APP_SERVER_HEALTH_RETRY_MS);\n }\n\n return false;\n}\n\nexport async function checkManagedAppServerReady(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n): Promise<boolean> {\n const readyzStatus = await checkAppServerReadyz(url, timeoutMs);\n if (readyzStatus === \"ready\") {\n return true;\n }\n\n if (readyzStatus === \"unsupported\") {\n return checkTcpPortListening(url, timeoutMs);\n }\n\n return false;\n}\n\nexport async function waitForManagedAppServerReady(\n url: string,\n timeoutMs: number,\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n const remaining = Math.max(\n 1,\n Math.min(APP_SERVER_HEALTH_TIMEOUT_MS, deadline - Date.now()),\n );\n if (await checkManagedAppServerReady(url, remaining)) {\n return true;\n }\n await delay(APP_SERVER_HEALTH_RETRY_MS);\n }\n\n return false;\n}\n\nexport function markAppServerHealthy(\n appServer: AppServerState,\n): AppServerState {\n const checkedAt = new Date().toISOString();\n return {\n ...appServer,\n healthy: true,\n lastCheckedAt: checkedAt,\n lastHealthyAt: checkedAt,\n };\n}\n"],"mappings":"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","names":["resolve"]}
1
+ {"version":3,"sources":["../../src/bridges/codex-app-server-auth-gateway.ts","../../src/engine/bridge-app-server-health.ts"],"sourcesContent":["import {\n createServer,\n type IncomingMessage,\n type Server as HttpServer,\n type ServerResponse,\n} from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { Duplex } from \"node:stream\";\nimport { pathToFileURL } from \"node:url\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport { WebSocket, WebSocketServer, type RawData } from \"ws\";\nimport { checkManagedAppServerReady } from \"../engine/bridge-app-server-health.js\";\n\nconst AUTH_SUBPROTOCOL_PREFIX = \"tap-auth-\";\nconst CLOSE_UNAUTHORIZED = 4401;\nconst CLOSE_UPSTREAM_ERROR = 1013;\nexport const GATEWAY_READYZ_PATH = \"/readyz\";\n\nexport interface GatewayOptions {\n listenUrl: string;\n upstreamUrl: string;\n token: string;\n}\n\nexport interface GatewayRuntime {\n server: HttpServer;\n close(): Promise<void>;\n}\n\nfunction normalizeUrl(value: string): string {\n return value.replace(/\\/$/, \"\");\n}\n\nfunction closeSocket(\n socket: Pick<WebSocket, \"readyState\" | \"close\">,\n code: number,\n reason: string,\n): void {\n if (\n socket.readyState === WebSocket.CLOSING ||\n socket.readyState === WebSocket.CLOSED\n ) {\n return;\n }\n\n try {\n socket.close(code, reason);\n } catch {\n // Best-effort cleanup only.\n }\n}\n\nfunction readFlagValue(argv: string[], index: number, flag: string): string {\n const current = argv[index] ?? \"\";\n const eqIndex = current.indexOf(\"=\");\n if (eqIndex >= 0) {\n return current.slice(eqIndex + 1);\n }\n\n const next = argv[index + 1];\n if (!next || next.startsWith(\"--\")) {\n throw new Error(`Missing value for ${flag}`);\n }\n return next;\n}\n\nexport function buildGatewayOptions(argv: string[]): GatewayOptions {\n let listenUrl = process.env.TAP_GATEWAY_LISTEN_URL?.trim() || \"\";\n let upstreamUrl = process.env.TAP_GATEWAY_UPSTREAM_URL?.trim() || \"\";\n let tokenFile = process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || \"\";\n let token = process.env.TAP_GATEWAY_TOKEN?.trim() || \"\";\n\n for (let index = 0; index < argv.length; index += 1) {\n const flag = argv[index] ?? \"\";\n const consumesNext = !flag.includes(\"=\");\n\n if (flag.startsWith(\"--listen-url\")) {\n listenUrl = readFlagValue(argv, index, \"--listen-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--upstream-url\")) {\n upstreamUrl = readFlagValue(argv, index, \"--upstream-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token\")) {\n token = readFlagValue(argv, index, \"--token\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token-file\")) {\n tokenFile = readFlagValue(argv, index, \"--token-file\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n }\n\n if (tokenFile) {\n token = readFileSync(tokenFile, \"utf8\").trim();\n }\n\n if (!listenUrl) {\n throw new Error(\"Missing gateway listen URL\");\n }\n if (!upstreamUrl) {\n throw new Error(\"Missing gateway upstream URL\");\n }\n if (!token) {\n throw new Error(\"Missing gateway auth token\");\n }\n\n const listen = new URL(listenUrl);\n const upstream = new URL(upstreamUrl);\n if (!/^wss?:$/.test(listen.protocol)) {\n throw new Error(`Unsupported gateway listen protocol: ${listen.protocol}`);\n }\n if (!/^wss?:$/.test(upstream.protocol)) {\n throw new Error(\n `Unsupported gateway upstream protocol: ${upstream.protocol}`,\n );\n }\n\n return {\n listenUrl: normalizeUrl(listen.toString()),\n upstreamUrl: normalizeUrl(upstream.toString()),\n token,\n };\n}\n\nfunction tokensMatch(\n presentedToken: string | null,\n expectedToken: string,\n): boolean {\n if (!presentedToken) {\n return false;\n }\n\n const presented = Buffer.from(presentedToken, \"utf8\");\n const expected = Buffer.from(expectedToken, \"utf8\");\n if (presented.length !== expected.length) {\n return false;\n }\n\n return timingSafeEqual(presented, expected);\n}\n\nasync function main(): Promise<void> {\n const options = buildGatewayOptions(process.argv.slice(2));\n const runtime = await startGatewayServer(options);\n\n const shutdown = () => {\n void runtime.close().finally(() => {\n process.exit(0);\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nfunction writeJson(\n response: ServerResponse,\n statusCode: number,\n body: Record<string, unknown>,\n): void {\n response.statusCode = statusCode;\n response.setHeader(\"Content-Type\", \"application/json\");\n response.end(JSON.stringify(body));\n}\n\nfunction writeUpgradeRequired(response: ServerResponse): void {\n response.statusCode = 426;\n response.setHeader(\"Connection\", \"Upgrade\");\n response.setHeader(\"Upgrade\", \"websocket\");\n response.end(\"Upgrade Required\");\n}\n\nfunction writeNotFound(response: ServerResponse): void {\n response.statusCode = 404;\n response.end(\"Not Found\");\n}\n\nfunction rejectUpgrade(socket: Duplex, statusCode: number): void {\n socket.write(\n `HTTP/1.1 ${statusCode} ${statusCode === 404 ? \"Not Found\" : \"Bad Request\"}\\r\\n\\r\\n`,\n );\n socket.destroy();\n}\n\n/**\n * Detect traversal sequences in any encoding: raw \"..\", percent-encoded\n * \"%2e%2e\", or mixed forms like \".%2e\" / \"%2e.\".\n */\nfunction containsTraversal(raw: string): boolean {\n if (raw.includes(\"..\")) return true;\n // Decode percent-encoded dots and recheck\n if (/%2e/i.test(raw) && raw.replace(/%2e/gi, \".\").includes(\"..\")) return true;\n return false;\n}\n\nfunction isUpgradePath(listenUrl: string, request: IncomingMessage): boolean {\n const requestUrl = new URL(\n request.url ?? \"/\",\n listenUrl.replace(/^ws/, \"http\"),\n );\n const listenPath = new URL(listenUrl).pathname;\n return requestUrl.pathname === (listenPath || \"/\");\n}\n\nasync function handleReadyzRequest(\n response: ServerResponse,\n options: GatewayOptions,\n): Promise<void> {\n const ready = await checkManagedAppServerReady(options.upstreamUrl);\n writeJson(response, ready ? 200 : 503, { ok: ready });\n}\n\nexport async function startGatewayServer(\n options: GatewayOptions,\n): Promise<GatewayRuntime> {\n const listen = new URL(options.listenUrl);\n const host = listen.hostname === \"localhost\" ? \"127.0.0.1\" : listen.hostname;\n const port = Number.parseInt(listen.port, 10);\n if (!Number.isFinite(port) || port <= 0) {\n throw new Error(\n `Gateway listen URL must include a valid port: ${options.listenUrl}`,\n );\n }\n\n const wsServer = new WebSocketServer({\n noServer: true,\n perMessageDeflate: false,\n });\n\n wsServer.on(\"connection\", (client: WebSocket, request: IncomingMessage) => {\n // Extract token from Sec-WebSocket-Protocol header (subprotocol auth).\n // Client sends: WebSocket(url, [\"tap-auth-<token>\"])\n // Falls back to query param for backward compatibility during migration.\n const protocols =\n request.headers[\"sec-websocket-protocol\"]\n ?.split(\",\")\n .map((s) => s.trim()) ?? [];\n const authProtocol = protocols.find((p) =>\n p.startsWith(AUTH_SUBPROTOCOL_PREFIX),\n );\n const subprotocolToken =\n authProtocol?.slice(AUTH_SUBPROTOCOL_PREFIX.length) ?? null;\n\n // Legacy fallback: query param (will be removed in future version)\n const requestUrl = new URL(request.url ?? \"/\", options.listenUrl);\n const queryToken = requestUrl.searchParams.get(\"tap_token\");\n\n const presentedToken = subprotocolToken ?? queryToken;\n if (!tokensMatch(presentedToken, options.token)) {\n closeSocket(client, CLOSE_UNAUTHORIZED, \"Unauthorized\");\n return;\n }\n\n const upstream = new WebSocket(options.upstreamUrl, {\n perMessageDeflate: false,\n });\n\n upstream.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n });\n\n client.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n }\n });\n\n upstream.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Upstream closed\";\n closeSocket(client, code || 1000, reason);\n });\n\n client.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Client closed\";\n closeSocket(upstream, code || 1000, reason);\n });\n\n upstream.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] upstream error: ${String(error)}`);\n closeSocket(client, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n closeSocket(upstream, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n });\n\n client.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] client error: ${String(error)}`);\n closeSocket(upstream, 1011, \"Client error\");\n });\n });\n\n const listenPath = new URL(options.listenUrl).pathname || \"/\";\n\n const server = createServer(async (request, response) => {\n const requestUrl = new URL(\n request.url ?? \"/\",\n options.listenUrl.replace(/^ws/, \"http\"),\n );\n\n // Defense-in-depth: reject traversal sequences in any encoding.\n // Covers raw \"..\", percent-encoded \"%2e%2e\", and mixed forms.\n if (containsTraversal(request.url ?? \"\")) {\n writeNotFound(response);\n return;\n }\n\n if (\n request.method === \"GET\" &&\n requestUrl.pathname === GATEWAY_READYZ_PATH\n ) {\n await handleReadyzRequest(response, options);\n return;\n }\n\n if (requestUrl.pathname === listenPath) {\n writeUpgradeRequired(response);\n return;\n }\n\n writeNotFound(response);\n });\n\n server.on(\"upgrade\", (request, socket, head) => {\n // Block traversal in upgrade requests (raw + encoded)\n if (containsTraversal(request.url ?? \"\")) {\n rejectUpgrade(socket, 400);\n return;\n }\n\n if (!isUpgradePath(options.listenUrl, request)) {\n rejectUpgrade(socket, 404);\n return;\n }\n\n wsServer.handleUpgrade(request, socket, head, (client) => {\n wsServer.emit(\"connection\", client, request);\n });\n });\n\n await new Promise<void>((resolvePromise, rejectPromise) => {\n server.once(\"error\", rejectPromise);\n server.listen(port, host, () => {\n server.off(\"error\", rejectPromise);\n console.log(\n `[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`,\n );\n resolvePromise();\n });\n });\n\n return {\n server,\n close() {\n return new Promise<void>((resolvePromise) => {\n server.close(() => {\n wsServer.close(() => resolvePromise());\n });\n });\n },\n };\n}\n\nfunction isDirectExecution(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n return import.meta.url === pathToFileURL(resolve(entry)).href;\n}\n\nif (isDirectExecution()) {\n main().catch((error) => {\n console.error(\n error instanceof Error ? (error.stack ?? error.message) : String(error),\n );\n process.exit(1);\n });\n}\n","import * as net from \"node:net\";\nimport type { AppServerState } from \"../types.js\";\nimport { getWebSocketCtor, delay } from \"./bridge-port-network.js\";\n\nexport interface WebSocketLike {\n addEventListener(\n type: \"open\" | \"error\" | \"close\",\n listener: () => void,\n options?: { once?: boolean },\n ): void;\n close(code?: number, reason?: string): void;\n}\n\nexport type WebSocketCtor = new (\n url: string,\n protocols?: string | string[],\n) => WebSocketLike;\n\nexport const APP_SERVER_HEALTH_TIMEOUT_MS = 1_500;\nexport const APP_SERVER_HEALTH_RETRY_MS = 250;\nexport const APP_SERVER_READYZ_PATH = \"/readyz\";\n\nexport const AUTH_SUBPROTOCOL_PREFIX = \"tap-auth-\";\n\nexport type AppServerReadyzStatus = \"ready\" | \"not-ready\" | \"unsupported\";\n\nexport async function checkAppServerHealth(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n gatewayToken?: string | null,\n): Promise<boolean> {\n const WebSocket = getWebSocketCtor();\n if (!WebSocket) {\n return false;\n }\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n let socket: WebSocketLike | null = null;\n\n const finish = (healthy: boolean) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n try {\n socket?.close();\n } catch {\n // Best-effort cleanup only.\n }\n resolve(healthy);\n };\n\n const timer = setTimeout(() => finish(false), timeoutMs);\n\n try {\n // Authenticate via WebSocket subprotocol when a gateway token is provided.\n const protocols = gatewayToken\n ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`]\n : undefined;\n socket = new WebSocket(url, protocols);\n socket.addEventListener(\"open\", () => finish(true), { once: true });\n socket.addEventListener(\"error\", () => finish(false), { once: true });\n socket.addEventListener(\"close\", () => finish(false), { once: true });\n } catch {\n finish(false);\n }\n });\n}\n\nexport async function waitForAppServerHealth(\n url: string,\n timeoutMs: number,\n gatewayToken?: string | null,\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n if (\n await checkAppServerHealth(\n url,\n APP_SERVER_HEALTH_TIMEOUT_MS,\n gatewayToken,\n )\n ) {\n return true;\n }\n await delay(APP_SERVER_HEALTH_RETRY_MS);\n }\n\n return false;\n}\n\nexport function buildAppServerReadyzUrl(url: string): string | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n\n if (parsed.protocol === \"ws:\") {\n parsed.protocol = \"http:\";\n } else if (parsed.protocol === \"wss:\") {\n parsed.protocol = \"https:\";\n } else if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return null;\n }\n\n parsed.pathname = APP_SERVER_READYZ_PATH;\n parsed.search = \"\";\n parsed.hash = \"\";\n return parsed.toString();\n}\n\nexport async function checkAppServerReadyz(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n): Promise<AppServerReadyzStatus> {\n const readyzUrl = buildAppServerReadyzUrl(url);\n if (!readyzUrl) {\n return \"unsupported\";\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(readyzUrl, {\n method: \"GET\",\n signal: controller.signal,\n headers: {\n accept: \"application/json\",\n },\n });\n\n if (response.ok) {\n return \"ready\";\n }\n\n if (\n response.status === 400 ||\n response.status === 404 ||\n response.status === 405 ||\n response.status === 426 ||\n response.status === 501\n ) {\n return \"unsupported\";\n }\n\n return \"not-ready\";\n } catch {\n return \"not-ready\";\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Check if a TCP port is accepting connections (without WebSocket upgrade).\n * Use this for managed startup health checks to avoid creating app-server sessions.\n */\nexport async function checkTcpPortListening(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n): Promise<boolean> {\n let hostname: string;\n let port: number;\n try {\n const parsed = new URL(url.replace(/^ws/, \"http\"));\n hostname = parsed.hostname;\n port = parseInt(parsed.port, 10);\n } catch {\n return false;\n }\n if (!port || !Number.isFinite(port)) return false;\n\n return new Promise<boolean>((resolve) => {\n const socket = net.createConnection({ host: hostname, port });\n const timer = setTimeout(() => {\n socket.destroy();\n resolve(false);\n }, timeoutMs);\n\n socket.once(\"connect\", () => {\n clearTimeout(timer);\n socket.destroy();\n resolve(true);\n });\n socket.once(\"error\", () => {\n clearTimeout(timer);\n socket.destroy();\n resolve(false);\n });\n });\n}\n\n/**\n * Wait for a TCP port to start accepting connections.\n * Does NOT open a WebSocket, so no app-server session is created.\n */\nexport async function waitForTcpPortListening(\n url: string,\n timeoutMs: number,\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n if (await checkTcpPortListening(url, APP_SERVER_HEALTH_TIMEOUT_MS)) {\n return true;\n }\n await delay(APP_SERVER_HEALTH_RETRY_MS);\n }\n\n return false;\n}\n\nexport async function checkManagedAppServerReady(\n url: string,\n timeoutMs: number = APP_SERVER_HEALTH_TIMEOUT_MS,\n): Promise<boolean> {\n const readyzStatus = await checkAppServerReadyz(url, timeoutMs);\n if (readyzStatus === \"ready\") {\n return true;\n }\n\n if (readyzStatus === \"unsupported\") {\n return checkTcpPortListening(url, timeoutMs);\n }\n\n return false;\n}\n\nexport async function waitForManagedAppServerReady(\n url: string,\n timeoutMs: number,\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n const remaining = Math.max(\n 1,\n Math.min(APP_SERVER_HEALTH_TIMEOUT_MS, deadline - Date.now()),\n );\n if (await checkManagedAppServerReady(url, remaining)) {\n return true;\n }\n await delay(APP_SERVER_HEALTH_RETRY_MS);\n }\n\n return false;\n}\n\nexport function markAppServerHealthy(\n appServer: AppServerState,\n): AppServerState {\n const checkedAt = new Date().toISOString();\n return {\n ...appServer,\n healthy: true,\n lastCheckedAt: checkedAt,\n lastHealthyAt: checkedAt,\n };\n}\n"],"mappings":"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","names":["resolve"]}
@@ -58,6 +58,10 @@ interface HeartbeatRecord {
58
58
  activeTurnId: string | null;
59
59
  turnStartedAt: string | null;
60
60
  lastTurnStatus: string | null;
61
+ lastTurnAt?: string | null;
62
+ lastDispatchAt?: string | null;
63
+ idleSince?: string | null;
64
+ turnState?: "active" | "idle" | "waiting-approval" | "disconnected";
61
65
  lastNotificationMethod: string | null;
62
66
  lastNotificationAt: string | null;
63
67
  lastError: string | null;
@@ -91,6 +95,14 @@ interface RequestRecord {
91
95
  interface HeartbeatStoreRecord {
92
96
  id?: string;
93
97
  agent?: string;
98
+ timestamp?: string;
99
+ lastActivity?: string;
100
+ joinedAt?: string;
101
+ status?: string;
102
+ source?: "bridge-dispatch" | "mcp-direct";
103
+ instanceId?: string | null;
104
+ bridgePid?: number | null;
105
+ connectHash?: string;
94
106
  }
95
107
  type HeartbeatStore = Record<string, HeartbeatStoreRecord>;
96
108
  interface JsonRpcResponse {