@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 +27 -87
- package/dist/bridges/codex-app-server-auth-gateway.mjs +23 -4
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +5 -224
- package/dist/bridges/codex-app-server-bridge.mjs +706 -1109
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +15 -1
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/bridges/gemini-ide-companion-runner.mjs +28 -21973
- package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -1
- package/dist/cli.mjs +2014 -646
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +59 -1
- package/dist/index.mjs +5550 -26566
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +1235 -21479
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +6 -4
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
|
-
> `
|
|
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
|
|
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
|
-
|
|
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.
|
|
120
|
+
"version": "0.x.y",
|
|
162
121
|
"commsDir": "/path/to/comms",
|
|
163
|
-
"
|
|
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
|
-
##
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
185
|
+
### Identity And Routing
|
|
248
186
|
|
|
249
|
-
- **
|
|
250
|
-
- **
|
|
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
|
-
###
|
|
190
|
+
### Bridge And Runtime Updates
|
|
254
191
|
|
|
255
|
-
|
|
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
|
-
|
|
197
|
+
### Test Hardening
|
|
258
198
|
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 {
|
|
73
|
+
export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, type LoadedThreadCandidate, buildOptions, buildUserInput, chooseLoadedThreadForCwd, isOwnMessageSender, loadResumableThreadState, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, threadCwdMatches, waitForTurnCompletion };
|