@hua-labs/tap 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -9
- package/dist/bridges/codex-app-server-auth-gateway.d.mts +9 -1
- package/dist/bridges/codex-app-server-auth-gateway.mjs +183 -14
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +224 -5
- package/dist/bridges/codex-app-server-bridge.mjs +1138 -687
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +17 -2
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +703 -95
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +502 -57
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +327 -70
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +6 -4
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -38,15 +38,6 @@ npx @hua-labs/tap status
|
|
|
38
38
|
|
|
39
39
|
Your agents can now communicate through the shared comms directory.
|
|
40
40
|
|
|
41
|
-
### Running Claude with tap
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
# Start Claude Code with tap MCP server
|
|
45
|
-
claude --mcp-config .mcp.json
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The `.mcp.json` file is auto-generated by `tap init`. It connects Claude to the tap communication layer, enabling `tap_reply`, `tap_who`, `tap_list_unread`, and other tools.
|
|
49
|
-
|
|
50
41
|
## Commands
|
|
51
42
|
|
|
52
43
|
### `init`
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
|
|
3
|
+
declare const GATEWAY_READYZ_PATH = "/readyz";
|
|
1
4
|
interface GatewayOptions {
|
|
2
5
|
listenUrl: string;
|
|
3
6
|
upstreamUrl: string;
|
|
4
7
|
token: string;
|
|
5
8
|
}
|
|
9
|
+
interface GatewayRuntime {
|
|
10
|
+
server: Server;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
6
13
|
declare function buildGatewayOptions(argv: string[]): GatewayOptions;
|
|
14
|
+
declare function startGatewayServer(options: GatewayOptions): Promise<GatewayRuntime>;
|
|
7
15
|
|
|
8
|
-
export { buildGatewayOptions };
|
|
16
|
+
export { GATEWAY_READYZ_PATH, type GatewayOptions, type GatewayRuntime, buildGatewayOptions, startGatewayServer };
|
|
@@ -1,12 +1,109 @@
|
|
|
1
1
|
// src/bridges/codex-app-server-auth-gateway.ts
|
|
2
|
+
import {
|
|
3
|
+
createServer
|
|
4
|
+
} from "http";
|
|
2
5
|
import { readFileSync } from "fs";
|
|
3
6
|
import { resolve } from "path";
|
|
4
7
|
import { pathToFileURL } from "url";
|
|
5
8
|
import { timingSafeEqual } from "crypto";
|
|
6
9
|
import { WebSocket, WebSocketServer } from "ws";
|
|
10
|
+
|
|
11
|
+
// src/engine/bridge-app-server-health.ts
|
|
12
|
+
import * as net from "net";
|
|
13
|
+
var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
14
|
+
var APP_SERVER_READYZ_PATH = "/readyz";
|
|
15
|
+
function buildAppServerReadyzUrl(url) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = new URL(url);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (parsed.protocol === "ws:") {
|
|
23
|
+
parsed.protocol = "http:";
|
|
24
|
+
} else if (parsed.protocol === "wss:") {
|
|
25
|
+
parsed.protocol = "https:";
|
|
26
|
+
} else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
parsed.pathname = APP_SERVER_READYZ_PATH;
|
|
30
|
+
parsed.search = "";
|
|
31
|
+
parsed.hash = "";
|
|
32
|
+
return parsed.toString();
|
|
33
|
+
}
|
|
34
|
+
async function checkAppServerReadyz(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
35
|
+
const readyzUrl = buildAppServerReadyzUrl(url);
|
|
36
|
+
if (!readyzUrl) {
|
|
37
|
+
return "unsupported";
|
|
38
|
+
}
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(readyzUrl, {
|
|
43
|
+
method: "GET",
|
|
44
|
+
signal: controller.signal,
|
|
45
|
+
headers: {
|
|
46
|
+
accept: "application/json"
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (response.ok) {
|
|
50
|
+
return "ready";
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 400 || response.status === 404 || response.status === 405 || response.status === 426 || response.status === 501) {
|
|
53
|
+
return "unsupported";
|
|
54
|
+
}
|
|
55
|
+
return "not-ready";
|
|
56
|
+
} catch {
|
|
57
|
+
return "not-ready";
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function checkTcpPortListening(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
63
|
+
let hostname;
|
|
64
|
+
let port;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = new URL(url.replace(/^ws/, "http"));
|
|
67
|
+
hostname = parsed.hostname;
|
|
68
|
+
port = parseInt(parsed.port, 10);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (!port || !Number.isFinite(port)) return false;
|
|
73
|
+
return new Promise((resolve2) => {
|
|
74
|
+
const socket = net.createConnection({ host: hostname, port });
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
socket.destroy();
|
|
77
|
+
resolve2(false);
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
socket.once("connect", () => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
socket.destroy();
|
|
82
|
+
resolve2(true);
|
|
83
|
+
});
|
|
84
|
+
socket.once("error", () => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
socket.destroy();
|
|
87
|
+
resolve2(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function checkManagedAppServerReady(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
92
|
+
const readyzStatus = await checkAppServerReadyz(url, timeoutMs);
|
|
93
|
+
if (readyzStatus === "ready") {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (readyzStatus === "unsupported") {
|
|
97
|
+
return checkTcpPortListening(url, timeoutMs);
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/bridges/codex-app-server-auth-gateway.ts
|
|
7
103
|
var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
|
|
8
104
|
var CLOSE_UNAUTHORIZED = 4401;
|
|
9
105
|
var CLOSE_UPSTREAM_ERROR = 1013;
|
|
106
|
+
var GATEWAY_READYZ_PATH = "/readyz";
|
|
10
107
|
function normalizeUrl(value) {
|
|
11
108
|
return value.replace(/\/$/, "");
|
|
12
109
|
}
|
|
@@ -101,6 +198,46 @@ function tokensMatch(presentedToken, expectedToken) {
|
|
|
101
198
|
}
|
|
102
199
|
async function main() {
|
|
103
200
|
const options = buildGatewayOptions(process.argv.slice(2));
|
|
201
|
+
const runtime = await startGatewayServer(options);
|
|
202
|
+
const shutdown = () => {
|
|
203
|
+
void runtime.close().finally(() => {
|
|
204
|
+
process.exit(0);
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
process.on("SIGINT", shutdown);
|
|
208
|
+
process.on("SIGTERM", shutdown);
|
|
209
|
+
}
|
|
210
|
+
function writeJson(response, statusCode, body) {
|
|
211
|
+
response.statusCode = statusCode;
|
|
212
|
+
response.setHeader("Content-Type", "application/json");
|
|
213
|
+
response.end(JSON.stringify(body));
|
|
214
|
+
}
|
|
215
|
+
function writeUpgradeRequired(response) {
|
|
216
|
+
response.statusCode = 426;
|
|
217
|
+
response.setHeader("Connection", "Upgrade");
|
|
218
|
+
response.setHeader("Upgrade", "websocket");
|
|
219
|
+
response.end("Upgrade Required");
|
|
220
|
+
}
|
|
221
|
+
function writeNotFound(response) {
|
|
222
|
+
response.statusCode = 404;
|
|
223
|
+
response.end("Not Found");
|
|
224
|
+
}
|
|
225
|
+
function rejectUpgrade(socket, statusCode) {
|
|
226
|
+
socket.write(`HTTP/1.1 ${statusCode} ${statusCode === 404 ? "Not Found" : "Bad Request"}\r
|
|
227
|
+
\r
|
|
228
|
+
`);
|
|
229
|
+
socket.destroy();
|
|
230
|
+
}
|
|
231
|
+
function isUpgradePath(listenUrl, request) {
|
|
232
|
+
const requestUrl = new URL(request.url ?? "/", listenUrl.replace(/^ws/, "http"));
|
|
233
|
+
const listenPath = new URL(listenUrl).pathname;
|
|
234
|
+
return requestUrl.pathname === (listenPath || "/");
|
|
235
|
+
}
|
|
236
|
+
async function handleReadyzRequest(response, options) {
|
|
237
|
+
const ready = await checkManagedAppServerReady(options.upstreamUrl);
|
|
238
|
+
writeJson(response, ready ? 200 : 503, { ok: ready });
|
|
239
|
+
}
|
|
240
|
+
async function startGatewayServer(options) {
|
|
104
241
|
const listen = new URL(options.listenUrl);
|
|
105
242
|
const host = listen.hostname === "localhost" ? "127.0.0.1" : listen.hostname;
|
|
106
243
|
const port = Number.parseInt(listen.port, 10);
|
|
@@ -109,13 +246,11 @@ async function main() {
|
|
|
109
246
|
`Gateway listen URL must include a valid port: ${options.listenUrl}`
|
|
110
247
|
);
|
|
111
248
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
port,
|
|
115
|
-
path: listen.pathname === "/" ? void 0 : listen.pathname,
|
|
249
|
+
const wsServer = new WebSocketServer({
|
|
250
|
+
noServer: true,
|
|
116
251
|
perMessageDeflate: false
|
|
117
252
|
});
|
|
118
|
-
|
|
253
|
+
wsServer.on("connection", (client, request) => {
|
|
119
254
|
const protocols = request.headers["sec-websocket-protocol"]?.split(",").map((s) => s.trim()) ?? [];
|
|
120
255
|
const authProtocol = protocols.find(
|
|
121
256
|
(p) => p.startsWith(AUTH_SUBPROTOCOL_PREFIX)
|
|
@@ -159,18 +294,50 @@ async function main() {
|
|
|
159
294
|
closeSocket(upstream, 1011, "Client error");
|
|
160
295
|
});
|
|
161
296
|
});
|
|
162
|
-
server
|
|
163
|
-
|
|
164
|
-
|
|
297
|
+
const server = createServer(async (request, response) => {
|
|
298
|
+
const requestUrl = new URL(
|
|
299
|
+
request.url ?? "/",
|
|
300
|
+
options.listenUrl.replace(/^ws/, "http")
|
|
165
301
|
);
|
|
302
|
+
if (request.method === "GET" && requestUrl.pathname === GATEWAY_READYZ_PATH) {
|
|
303
|
+
await handleReadyzRequest(response, options);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (isUpgradePath(options.listenUrl, request)) {
|
|
307
|
+
writeUpgradeRequired(response);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
writeNotFound(response);
|
|
166
311
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
312
|
+
server.on("upgrade", (request, socket, head) => {
|
|
313
|
+
if (!isUpgradePath(options.listenUrl, request)) {
|
|
314
|
+
rejectUpgrade(socket, 404);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
wsServer.handleUpgrade(request, socket, head, (client) => {
|
|
318
|
+
wsServer.emit("connection", client, request);
|
|
170
319
|
});
|
|
320
|
+
});
|
|
321
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
322
|
+
server.once("error", rejectPromise);
|
|
323
|
+
server.listen(port, host, () => {
|
|
324
|
+
server.off("error", rejectPromise);
|
|
325
|
+
console.log(
|
|
326
|
+
`[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`
|
|
327
|
+
);
|
|
328
|
+
resolvePromise();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
server,
|
|
333
|
+
close() {
|
|
334
|
+
return new Promise((resolvePromise) => {
|
|
335
|
+
server.close(() => {
|
|
336
|
+
wsServer.close(() => resolvePromise());
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
171
340
|
};
|
|
172
|
-
process.on("SIGINT", shutdown);
|
|
173
|
-
process.on("SIGTERM", shutdown);
|
|
174
341
|
}
|
|
175
342
|
function isDirectExecution() {
|
|
176
343
|
const entry = process.argv[1];
|
|
@@ -186,6 +353,8 @@ if (isDirectExecution()) {
|
|
|
186
353
|
});
|
|
187
354
|
}
|
|
188
355
|
export {
|
|
189
|
-
|
|
356
|
+
GATEWAY_READYZ_PATH,
|
|
357
|
+
buildGatewayOptions,
|
|
358
|
+
startGatewayServer
|
|
190
359
|
};
|
|
191
360
|
//# sourceMappingURL=codex-app-server-auth-gateway.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/bridges/codex-app-server-auth-gateway.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport { WebSocket, WebSocketServer, type RawData } from \"ws\";\n\nconst AUTH_SUBPROTOCOL_PREFIX = \"tap-auth-\";\nconst CLOSE_UNAUTHORIZED = 4401;\nconst CLOSE_UPSTREAM_ERROR = 1013;\n\ninterface GatewayOptions {\n listenUrl: string;\n upstreamUrl: string;\n token: string;\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 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 server = new WebSocketServer({\n host,\n port,\n path: listen.pathname === \"/\" ? undefined : listen.pathname,\n perMessageDeflate: false,\n });\n\n server.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 server.on(\"listening\", () => {\n console.log(\n `[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`,\n );\n });\n\n const shutdown = () => {\n server.close(() => {\n process.exit(0);\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\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"],"mappings":"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","names":[]}
|
|
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,4 +1,5 @@
|
|
|
1
1
|
type BusyMode = "wait" | "steer";
|
|
2
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
3
|
interface Options {
|
|
3
4
|
repoRoot: string;
|
|
4
5
|
commsDir: string;
|
|
@@ -17,9 +18,15 @@ interface Options {
|
|
|
17
18
|
gatewayToken: string | null;
|
|
18
19
|
gatewayTokenFile: string | null;
|
|
19
20
|
busyMode: BusyMode;
|
|
21
|
+
logLevel: LogLevel;
|
|
20
22
|
threadId: string | null;
|
|
21
23
|
ephemeral: boolean;
|
|
22
24
|
}
|
|
25
|
+
interface InboxRoute {
|
|
26
|
+
sender: string;
|
|
27
|
+
recipient: string;
|
|
28
|
+
subject: string;
|
|
29
|
+
}
|
|
23
30
|
interface Candidate {
|
|
24
31
|
markerId: string;
|
|
25
32
|
filePath: string;
|
|
@@ -37,6 +44,31 @@ interface ThreadStateRecord {
|
|
|
37
44
|
ephemeral: boolean;
|
|
38
45
|
cwd?: string | null;
|
|
39
46
|
}
|
|
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
|
+
}
|
|
40
72
|
interface HeadlessWarmupClient {
|
|
41
73
|
activeTurnId: string | null;
|
|
42
74
|
lastTurnStatus: string | null;
|
|
@@ -50,24 +82,211 @@ interface LoadedThreadCandidate {
|
|
|
50
82
|
statusType: string | null;
|
|
51
83
|
thread: any;
|
|
52
84
|
}
|
|
85
|
+
interface RequestRecord {
|
|
86
|
+
jsonrpc: "2.0";
|
|
87
|
+
id: number;
|
|
88
|
+
method: string;
|
|
89
|
+
params: unknown;
|
|
90
|
+
}
|
|
53
91
|
interface HeartbeatStoreRecord {
|
|
54
92
|
id?: string;
|
|
55
93
|
agent?: string;
|
|
56
94
|
}
|
|
57
95
|
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>;
|
|
58
111
|
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;
|
|
59
126
|
declare function threadCwdMatches(expectedCwd: string, actualCwd: string | null | undefined): boolean;
|
|
60
127
|
declare function chooseLoadedThreadForCwd(cwd: string, threads: LoadedThreadCandidate[]): LoadedThreadCandidate | null;
|
|
128
|
+
declare function normalizeAgentToken(value?: string | null): string | null;
|
|
61
129
|
declare function resolveAgentId(preferredAgentName?: string | null): string;
|
|
62
|
-
declare function
|
|
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
|
+
*/
|
|
63
146
|
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
|
+
*/
|
|
64
151
|
declare function isOwnMessageSender(sender: string, agentId: string, agentName: string): boolean;
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
|
|
67
217
|
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>;
|
|
68
283
|
declare function waitForTurnCompletion(client: Pick<HeadlessWarmupClient, "activeTurnId" | "lastTurnStatus" | "refreshCurrentThreadState">, turnId: string, timeoutMs: number): Promise<string | null>;
|
|
69
284
|
declare function maybeBootstrapHeadlessTurn(options: Options, cutoff: Date, client: HeadlessWarmupClient): Promise<boolean>;
|
|
70
|
-
|
|
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;
|
|
71
289
|
declare function main(): Promise<void>;
|
|
290
|
+
declare function isDirectExecution(): boolean;
|
|
72
291
|
|
|
73
|
-
export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, type LoadedThreadCandidate, buildOptions, buildUserInput, chooseLoadedThreadForCwd, isOwnMessageSender, loadResumableThreadState, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, threadCwdMatches, waitForTurnCompletion };
|
|
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 };
|