@hua-labs/tap 0.4.2 → 0.5.0

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,37 @@ 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
221
-
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`
177
+ ## Recent Changes
226
178
 
227
- ### Web Dashboard
228
-
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
- ```
179
+ ### Config And Lifecycle
244
180
 
245
- Continuous health monitoring with auto-restart for stuck bridges. Cron/systemd friendly.
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
246
184
 
247
- ### Cross-Platform
185
+ ### Identity And Routing
248
186
 
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)
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
252
189
 
253
- ### Modular Architecture
190
+ ### Bridge And Runtime Updates
254
191
 
255
- bridge.ts split from 1,744 to 241 lines (-86%) across 16 focused modules. See `docs/areas/tap/splitting-convention.md`.
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
256
196
 
257
- ## Examples
197
+ ### Test Hardening
258
198
 
259
- Real multi-agent collaboration highlights from 18 generations:
199
+ - **CLI-path coverage** integration tests now exercise the actual `bridge` and `up` command paths that patch Codex `approval_mode`
200
+ - **Publish prep stabilization** — failing suites were fixed or quarantined so release-blocking regressions show up earlier in the main package tests
260
201
 
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)
202
+ ## Migration Notes
266
203
 
267
- [See all 10 examples →](examples/)
204
+ - **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.
205
+ - **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`.
206
+ - **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.
207
+ - **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.
268
208
 
269
209
  ## License
270
210
 
@@ -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"]}
@@ -1,5 +1,4 @@
1
1
  type BusyMode = "wait" | "steer";
2
- type LogLevel = "debug" | "info" | "warn" | "error";
3
2
  interface Options {
4
3
  repoRoot: string;
5
4
  commsDir: string;
@@ -18,15 +17,9 @@ interface Options {
18
17
  gatewayToken: string | null;
19
18
  gatewayTokenFile: string | null;
20
19
  busyMode: BusyMode;
21
- logLevel: LogLevel;
22
20
  threadId: string | null;
23
21
  ephemeral: boolean;
24
22
  }
25
- interface InboxRoute {
26
- sender: string;
27
- recipient: string;
28
- subject: string;
29
- }
30
23
  interface Candidate {
31
24
  markerId: string;
32
25
  filePath: string;
@@ -44,31 +37,6 @@ interface ThreadStateRecord {
44
37
  ephemeral: boolean;
45
38
  cwd?: string | null;
46
39
  }
47
- interface HeartbeatRecord {
48
- pid: number;
49
- agent: string;
50
- updatedAt: string;
51
- pollSeconds: number;
52
- appServerUrl: string;
53
- authenticated: boolean;
54
- connected: boolean;
55
- initialized: boolean;
56
- threadId: string | null;
57
- threadCwd?: string | null;
58
- activeTurnId: string | null;
59
- turnStartedAt: string | null;
60
- lastTurnStatus: string | null;
61
- lastNotificationMethod: string | null;
62
- lastNotificationAt: string | null;
63
- lastError: string | null;
64
- lastSuccessfulAppServerAt: string | null;
65
- lastSuccessfulAppServerMethod: string | null;
66
- consecutiveFailureCount: number;
67
- busyMode: BusyMode;
68
- }
69
- interface BridgeHealthState {
70
- consecutiveFailureCount: number;
71
- }
72
40
  interface HeadlessWarmupClient {
73
41
  activeTurnId: string | null;
74
42
  lastTurnStatus: string | null;
@@ -82,211 +50,24 @@ interface LoadedThreadCandidate {
82
50
  statusType: string | null;
83
51
  thread: any;
84
52
  }
85
- interface RequestRecord {
86
- jsonrpc: "2.0";
87
- id: number;
88
- method: string;
89
- params: unknown;
90
- }
91
53
  interface HeartbeatStoreRecord {
92
54
  id?: string;
93
55
  agent?: string;
94
56
  }
95
57
  type HeartbeatStore = Record<string, HeartbeatStoreRecord>;
96
- interface JsonRpcResponse {
97
- id?: number;
98
- result?: any;
99
- error?: {
100
- code?: number;
101
- message?: string;
102
- data?: unknown;
103
- };
104
- method?: string;
105
- params?: any;
106
- }
107
- declare const DEFAULT_AGENT: string;
108
- declare const DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
109
- declare const AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
110
- declare const PLACEHOLDER_AGENT_VALUES: Set<string>;
111
58
  declare const HEADLESS_WARMUP_PROMPT: string;
112
- declare const HEADLESS_WARMUP_TIMEOUT_MS = 30000;
113
- declare const TURN_COMPLETION_POLL_MS = 250;
114
- declare const TURN_COMPLETION_REFRESH_MS = 1000;
115
- declare const HEADLESS_SKIP_PATTERNS: RegExp[];
116
- declare const COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2000;
117
- declare const COMMS_LOCK_STALE_AGE_MS = 10000;
118
- /** M203: Timeout after which an active turn is considered stale (5 minutes). */
119
- declare const STALE_TURN_MS: number;
120
-
121
- /**
122
- * M206: Re-export canonicalizeAgentId as canonicalize for backward compat.
123
- */
124
- declare function canonicalize(id: string): string;
125
- declare function normalizeThreadCwd(cwd: string): string;
126
59
  declare function threadCwdMatches(expectedCwd: string, actualCwd: string | null | undefined): boolean;
127
60
  declare function chooseLoadedThreadForCwd(cwd: string, threads: LoadedThreadCandidate[]): LoadedThreadCandidate | null;
128
- declare function normalizeAgentToken(value?: string | null): string | null;
129
61
  declare function resolveAgentId(preferredAgentName?: string | null): string;
130
- declare function resolveAgentName(preferredAgentName: string | null, stateDir: string): string;
131
- declare function resolveCurrentAgentName(agentId: string, fallbackAgentName: string, heartbeats: HeartbeatStore): string;
132
- declare function resolveAddressLabel(address: string, heartbeats: HeartbeatStore): string;
133
- declare function persistAgentName(stateDir: string, agentName: string): void;
134
- declare function formatAgentLabel(agentIdOrName: string, displayName?: string | null): string;
135
- /**
136
- * Resolve the current display name from heartbeats and persist if changed.
137
- * Returns the resolved name WITHOUT mutating options.agentName — callers
138
- * should use the return value for the current scan cycle only.
139
- * This prevents recipient matching from losing the original configured name.
140
- */
141
- declare function refreshAgentIdentity(options: Options, heartbeats: HeartbeatStore): string;
142
- /**
143
- * M206: Delegate to shared tap-identity helper.
144
- * Kept as named export for barrel backward compatibility.
145
- */
62
+ declare function loadResumableThreadState(stateDir: string, fallbackAppServerUrl: string): ThreadStateRecord | null;
146
63
  declare function recipientMatchesAgent(recipient: string, agentId: string, agentName: string): boolean;
147
- /**
148
- * M206: Delegate to shared tap-identity helper.
149
- * Kept as named export for barrel backward compatibility.
150
- */
151
64
  declare function isOwnMessageSender(sender: string, agentId: string, agentName: string): boolean;
152
- /**
153
- * M203: Check if a turn's activeFlags indicate it cannot accept steer.
154
- * Returns true if the turn should be treated as not active.
155
- */
156
- declare function isTurnStuckOnApproval(activeFlags: string[]): boolean;
157
- /**
158
- * M203: Check if a turn has been running longer than the stale threshold.
159
- */
160
- declare function isTurnStale(turnStartedAt: string | null, nowMs?: number): boolean;
161
- declare function shouldRetrySteerAsStart(error: unknown): boolean;
162
- /**
163
- * Parse YAML frontmatter from message content for routing.
164
- * Returns null if no valid frontmatter found.
165
- */
166
- declare function parseBridgeFrontmatter(content: string): {
167
- sender: string;
168
- recipient: string;
169
- subject: string;
170
- } | null;
171
- /**
172
- * Strip YAML frontmatter from message content, returning only the body.
173
- */
174
- declare function stripBridgeFrontmatter(content: string): string;
175
- declare function getInboxRoute(fileName: string, body?: string): InboxRoute;
176
- declare function getInboxRouteFromFilename(fileName: string): InboxRoute;
177
-
178
- declare function parseArgs(argv: string[]): {
179
- repoRoot?: string;
180
- commsDir?: string;
181
- agentName?: string;
182
- stateDir?: string;
183
- pollSeconds?: number;
184
- reconnectSeconds?: number;
185
- messageLookbackMinutes?: number;
186
- processExistingMessages: boolean;
187
- dryRun: boolean;
188
- runOnce: boolean;
189
- waitAfterDispatchSeconds?: number;
190
- appServerUrl?: string;
191
- gatewayTokenFile?: string;
192
- busyMode?: BusyMode;
193
- logLevel?: LogLevel;
194
- threadId?: string;
195
- ephemeral: boolean;
196
- };
197
- declare function resolveRepoRoot(explicit?: string): string;
198
- declare function resolveTapConfigPath(repoRoot: string, input: string): string;
199
- declare function resolveCommsDir(repoRoot: string, explicit?: string): string;
200
- declare function resolvePreferredAgentName(requested?: string): string | null;
201
- declare function sanitizeStateSegment(agentName: string): string;
202
- declare function buildDefaultStateDir(repoRoot: string, preferredAgentName?: string | null): string;
203
- declare function resolveStateDir(repoRoot: string, explicit?: string, preferredAgentName?: string | null): string;
204
- declare function readGatewayTokenFile(tokenFile: string): string;
205
- declare function buildOptions(argv: string[]): Options;
206
-
207
- declare function buildMarkerId(filePath: string, mtimeMs: number): string;
208
- declare function getProcessedMarkerPath(stateDir: string, markerId: string): string;
209
- declare function loadHeartbeats(commsDir: string): HeartbeatStore;
210
- declare function shouldSkipInHeadlessMode(fileName: string, body: string): boolean;
211
- declare function collectCandidates(inboxDir: string, agentId: string, agentName: string, aliasName?: string): Candidate[];
212
- declare function getPendingCandidates(options: Options, cutoff: Date): {
213
- heartbeats: HeartbeatStore;
214
- candidates: Candidate[];
215
- };
216
-
65
+ declare function resolveAddressLabel(address: string, heartbeats: HeartbeatStore): string;
66
+ declare function resolveCurrentAgentName(agentId: string, fallbackAgentName: string, heartbeats: HeartbeatStore): string;
217
67
  declare function buildUserInput(candidate: Candidate, agentName: string, heartbeats: HeartbeatStore): string;
218
- declare function writeProcessedMarker(stateDir: string, candidate: Candidate, dispatchMode: "start" | "steer", threadId: string | null, turnId: string | null): void;
219
- declare function writeLastDispatch(stateDir: string, candidate: Candidate, dispatchMode: "start" | "steer", threadId: string | null, turnId: string | null): void;
220
-
221
- type LogContext = Record<string, unknown>;
222
- interface BridgeLogger {
223
- debug(message: string, context?: LogContext): void;
224
- info(message: string, context?: LogContext): void;
225
- warn(message: string, context?: LogContext): void;
226
- error(message: string, context?: LogContext): void;
227
- }
228
-
229
- declare function readSocketData(data: unknown): Promise<string>;
230
- declare function formatJsonRpcError(error: JsonRpcResponse["error"]): string;
231
- declare class AppServerClient {
232
- private socket;
233
- private readonly url;
234
- private readonly gatewayToken;
235
- private readonly logger;
236
- private readonly clientId;
237
- private nextId;
238
- private pending;
239
- connected: boolean;
240
- initialized: boolean;
241
- threadId: string | null;
242
- currentThreadCwd: string | null;
243
- activeTurnId: string | null;
244
- turnStartedAt: string | null;
245
- lastTurnStatus: string | null;
246
- lastNotificationMethod: string | null;
247
- lastNotificationAt: string | null;
248
- lastError: string | null;
249
- lastSuccessfulAppServerAt: string | null;
250
- lastSuccessfulAppServerMethod: string | null;
251
- constructor(url: string, logger: BridgeLogger, gatewayToken?: string | null);
252
- connect(): Promise<void>;
253
- disconnect(): Promise<void>;
254
- ensureThread(explicitThreadId: string | null, savedThread: ThreadStateRecord | null, cwd: string, ephemeral: boolean): Promise<string>;
255
- findLoadedThread(cwd: string): Promise<string | null>;
256
- startTurn(inputText: string): Promise<string | null>;
257
- steerTurn(inputText: string): Promise<string>;
258
- isBusy(): boolean;
259
- refreshCurrentThreadState(): Promise<void>;
260
- private requireThreadId;
261
- private requireActiveTurnId;
262
- private refreshThreadState;
263
- private syncThreadStateFromThread;
264
- private handleMessage;
265
- private handleNotification;
266
- private request;
267
- private rejectPending;
268
- }
269
-
270
- declare function sanitizeErrorForPersistence(error: string | null): string | null;
271
- declare function readThreadState(stateDir: string): ThreadStateRecord | null;
272
- declare function persistThreadState(stateDir: string, threadId: string, appServerUrl: string, ephemeral: boolean, cwd: string | null): void;
273
- declare function acquireCommsLock(lockPath: string): boolean;
274
- declare function releaseCommsLock(lockPath: string): void;
275
- declare function updateCommsHeartbeat(options: Options, status: string): void;
276
- declare function writeHeartbeat(options: Options, client: AppServerClient | null, health: BridgeHealthState): void;
277
- declare function dispatchCandidate(client: AppServerClient, options: Options, candidate: Candidate, heartbeats: HeartbeatStore): Promise<boolean>;
278
- declare function runScan(options: Options, cutoff: Date, client: AppServerClient | null): Promise<{
279
- dispatched: boolean;
280
- maxMtimeMs: number;
281
- }>;
282
- declare function waitForTurnDrain(options: Options, client: AppServerClient, health: BridgeHealthState): Promise<void>;
283
68
  declare function waitForTurnCompletion(client: Pick<HeadlessWarmupClient, "activeTurnId" | "lastTurnStatus" | "refreshCurrentThreadState">, turnId: string, timeoutMs: number): Promise<string | null>;
284
69
  declare function maybeBootstrapHeadlessTurn(options: Options, cutoff: Date, client: HeadlessWarmupClient): Promise<boolean>;
285
-
286
- declare function readHeartbeatState(stateDir: string): HeartbeatRecord | null;
287
- declare function loadResumableThreadState(stateDir: string, fallbackAppServerUrl: string): ThreadStateRecord | null;
288
- declare function getGeneralInboxCutoff(stateDir: string, lookbackMinutes: number, processExistingMessages: boolean): Date;
70
+ declare function buildOptions(argv: string[]): Options;
289
71
  declare function main(): Promise<void>;
290
- declare function isDirectExecution(): boolean;
291
72
 
292
- export { AUTH_SUBPROTOCOL_PREFIX, AppServerClient, type BridgeHealthState, type BusyMode, COMMS_HEARTBEAT_LOCK_TIMEOUT_MS, COMMS_LOCK_STALE_AGE_MS, type Candidate, DEFAULT_AGENT, DEFAULT_APP_SERVER_URL, HEADLESS_SKIP_PATTERNS, HEADLESS_WARMUP_PROMPT, HEADLESS_WARMUP_TIMEOUT_MS, type HeadlessWarmupClient, type HeartbeatRecord, type HeartbeatStore, type HeartbeatStoreRecord, type InboxRoute, type JsonRpcResponse, type LoadedThreadCandidate, type LogLevel, type Options, PLACEHOLDER_AGENT_VALUES, type RequestRecord, STALE_TURN_MS, TURN_COMPLETION_POLL_MS, TURN_COMPLETION_REFRESH_MS, type ThreadStateRecord, acquireCommsLock, buildDefaultStateDir, buildMarkerId, buildOptions, buildUserInput, canonicalize, chooseLoadedThreadForCwd, collectCandidates, dispatchCandidate, formatAgentLabel, formatJsonRpcError, getGeneralInboxCutoff, getInboxRoute, getInboxRouteFromFilename, getPendingCandidates, getProcessedMarkerPath, isDirectExecution, isOwnMessageSender, isTurnStale, isTurnStuckOnApproval, loadHeartbeats, loadResumableThreadState, main, maybeBootstrapHeadlessTurn, normalizeAgentToken, normalizeThreadCwd, parseArgs, parseBridgeFrontmatter, persistAgentName, persistThreadState, readGatewayTokenFile, readHeartbeatState, readSocketData, readThreadState, recipientMatchesAgent, refreshAgentIdentity, releaseCommsLock, resolveAddressLabel, resolveAgentId, resolveAgentName, resolveCommsDir, resolveCurrentAgentName, resolvePreferredAgentName, resolveRepoRoot, resolveStateDir, resolveTapConfigPath, runScan, sanitizeErrorForPersistence, sanitizeStateSegment, shouldRetrySteerAsStart, shouldSkipInHeadlessMode, stripBridgeFrontmatter, threadCwdMatches, updateCommsHeartbeat, waitForTurnCompletion, waitForTurnDrain, writeHeartbeat, writeLastDispatch, writeProcessedMarker };
73
+ export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, type LoadedThreadCandidate, buildOptions, buildUserInput, chooseLoadedThreadForCwd, isOwnMessageSender, loadResumableThreadState, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, threadCwdMatches, waitForTurnCompletion };