@hua-labs/tap 0.2.2 → 0.2.4
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/dist/bridges/codex-app-server-auth-gateway.mjs +11 -3
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +1 -0
- package/dist/bridges/codex-app-server-bridge.mjs +16 -13
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +21 -17
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +497 -163
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +186 -92
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +185 -185
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -88,7 +88,7 @@ interface HeadlessConfig {
|
|
|
88
88
|
qualitySeverityFloor: "critical" | "high" | "medium";
|
|
89
89
|
}
|
|
90
90
|
interface AppServerAuthState {
|
|
91
|
-
mode: "query-token";
|
|
91
|
+
mode: "subprotocol" | "query-token";
|
|
92
92
|
protectedUrl: string;
|
|
93
93
|
upstreamUrl: string;
|
|
94
94
|
tokenPath: string;
|
|
@@ -442,6 +442,8 @@ declare function getConfig(options?: StateApiOptions): {
|
|
|
442
442
|
interface HttpServerOptions extends StateApiOptions {
|
|
443
443
|
/** Port to listen on (default: 4580) */
|
|
444
444
|
port?: number;
|
|
445
|
+
/** Pre-set API token (default: auto-generated) */
|
|
446
|
+
token?: string;
|
|
445
447
|
}
|
|
446
448
|
/**
|
|
447
449
|
* Start a localhost-only HTTP server for the tap State API.
|
|
@@ -449,6 +451,7 @@ interface HttpServerOptions extends StateApiOptions {
|
|
|
449
451
|
*/
|
|
450
452
|
declare function startHttpServer(options?: HttpServerOptions): Promise<{
|
|
451
453
|
port: number;
|
|
454
|
+
token: string;
|
|
452
455
|
close: () => Promise<void>;
|
|
453
456
|
}>;
|
|
454
457
|
|
package/dist/index.mjs
CHANGED
|
@@ -27,8 +27,8 @@ function findRepoRoot(startDir = process.cwd()) {
|
|
|
27
27
|
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
28
28
|
if (!_noGitWarned) {
|
|
29
29
|
_setNoGitWarned();
|
|
30
|
-
|
|
31
|
-
"No .git directory found. Resolved
|
|
30
|
+
log(
|
|
31
|
+
"No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
return dir;
|
|
@@ -39,8 +39,8 @@ function findRepoRoot(startDir = process.cwd()) {
|
|
|
39
39
|
}
|
|
40
40
|
if (!_noGitWarned) {
|
|
41
41
|
_setNoGitWarned();
|
|
42
|
-
|
|
43
|
-
"No git repository or package.json found. Using current directory as root.
|
|
42
|
+
log(
|
|
43
|
+
"No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
return process.cwd();
|
|
@@ -82,9 +82,6 @@ function log(message) {
|
|
|
82
82
|
function logSuccess(message) {
|
|
83
83
|
if (!_jsonMode) console.log(` + ${message}`);
|
|
84
84
|
}
|
|
85
|
-
function logWarn(message) {
|
|
86
|
-
if (!_jsonMode) console.log(` ! ${message}`);
|
|
87
|
-
}
|
|
88
85
|
function logError(message) {
|
|
89
86
|
if (!_jsonMode) console.error(` x ${message}`);
|
|
90
87
|
}
|
|
@@ -93,6 +90,16 @@ function logHeader(message) {
|
|
|
93
90
|
${message}
|
|
94
91
|
`);
|
|
95
92
|
}
|
|
93
|
+
function parseIntFlag(value, name, min, max) {
|
|
94
|
+
if (value === void 0) return void 0;
|
|
95
|
+
const parsed = Number(value);
|
|
96
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
97
|
+
throw new RangeError(
|
|
98
|
+
`Invalid ${name}: ${value}. Must be an integer between ${min} and ${max}.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
96
103
|
function resolveInstanceId(identifier, state) {
|
|
97
104
|
if (state.instances[identifier]) {
|
|
98
105
|
return { ok: true, instanceId: identifier };
|
|
@@ -140,8 +147,8 @@ function findRepoRoot2(startDir = process.cwd()) {
|
|
|
140
147
|
if (fs2.existsSync(path2.join(dir, "package.json"))) {
|
|
141
148
|
if (!_noGitWarned) {
|
|
142
149
|
_setNoGitWarned();
|
|
143
|
-
|
|
144
|
-
"
|
|
150
|
+
log(
|
|
151
|
+
"No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
145
152
|
);
|
|
146
153
|
}
|
|
147
154
|
return dir;
|
|
@@ -152,8 +159,8 @@ function findRepoRoot2(startDir = process.cwd()) {
|
|
|
152
159
|
}
|
|
153
160
|
if (!_noGitWarned) {
|
|
154
161
|
_setNoGitWarned();
|
|
155
|
-
|
|
156
|
-
"
|
|
162
|
+
log(
|
|
163
|
+
"No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
157
164
|
);
|
|
158
165
|
}
|
|
159
166
|
return process.cwd();
|
|
@@ -556,18 +563,18 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
556
563
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
557
564
|
}
|
|
558
565
|
const isBundled = sourcePath.endsWith(".mjs");
|
|
566
|
+
const isEphemeralSource = isEphemeralPath(sourcePath);
|
|
559
567
|
let command = bunCommand;
|
|
560
568
|
let args = [toForwardSlashPath(sourcePath)];
|
|
561
|
-
if (
|
|
562
|
-
|
|
569
|
+
if (isEphemeralSource && isBundled) {
|
|
570
|
+
command = "npx";
|
|
571
|
+
args = ["@hua-labs/tap", "serve"];
|
|
572
|
+
warnings.push(
|
|
573
|
+
"Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
|
|
574
|
+
);
|
|
575
|
+
} else if (!command && isBundled) {
|
|
563
576
|
const isEphemeralNode = isEphemeralPath(process.execPath);
|
|
564
|
-
if (
|
|
565
|
-
command = "npx";
|
|
566
|
-
args = ["@hua-labs/tap", "serve"];
|
|
567
|
-
warnings.push(
|
|
568
|
-
"Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
|
|
569
|
-
);
|
|
570
|
-
} else if (isEphemeralNode) {
|
|
577
|
+
if (isEphemeralNode) {
|
|
571
578
|
command = "node";
|
|
572
579
|
warnings.push(
|
|
573
580
|
"Detected ephemeral node path. Using `node` from PATH for MCP config stability."
|
|
@@ -575,11 +582,9 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
575
582
|
} else {
|
|
576
583
|
command = toForwardSlashPath(process.execPath);
|
|
577
584
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
);
|
|
582
|
-
}
|
|
585
|
+
warnings.push(
|
|
586
|
+
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
587
|
+
);
|
|
583
588
|
}
|
|
584
589
|
if (!command) {
|
|
585
590
|
issues.push(
|
|
@@ -840,8 +845,11 @@ function resolvePowerShellCommand() {
|
|
|
840
845
|
function resolveAuthGatewayScript(repoRoot) {
|
|
841
846
|
const moduleDir = path7.dirname(fileURLToPath3(import.meta.url));
|
|
842
847
|
const candidates = [
|
|
843
|
-
|
|
844
|
-
path7.join(moduleDir, "
|
|
848
|
+
// Bundled: dist/bridges/ sibling (npm install / built package)
|
|
849
|
+
path7.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
|
|
850
|
+
// Source: src/bridges/ sibling (monorepo dev with ts runner)
|
|
851
|
+
path7.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
|
|
852
|
+
// Monorepo dist fallback
|
|
845
853
|
path7.join(
|
|
846
854
|
repoRoot,
|
|
847
855
|
"packages",
|
|
@@ -894,10 +902,8 @@ async function allocateLoopbackPort(hostname) {
|
|
|
894
902
|
});
|
|
895
903
|
});
|
|
896
904
|
}
|
|
897
|
-
function buildProtectedAppServerUrl(publicUrl,
|
|
898
|
-
|
|
899
|
-
url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
|
|
900
|
-
return url.toString().replace(/\/(?=\?|$)/, "");
|
|
905
|
+
function buildProtectedAppServerUrl(publicUrl, _token) {
|
|
906
|
+
return publicUrl;
|
|
901
907
|
}
|
|
902
908
|
function readGatewayTokenFromPath(tokenPath) {
|
|
903
909
|
return fs7.readFileSync(tokenPath, "utf8").trim();
|
|
@@ -1012,7 +1018,7 @@ async function createManagedAppServerAuth(options) {
|
|
|
1012
1018
|
throw new Error("Failed to spawn app-server auth gateway");
|
|
1013
1019
|
}
|
|
1014
1020
|
return {
|
|
1015
|
-
mode: "
|
|
1021
|
+
mode: "subprotocol",
|
|
1016
1022
|
protectedUrl,
|
|
1017
1023
|
upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
|
|
1018
1024
|
tokenPath,
|
|
@@ -1199,7 +1205,7 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
|
|
|
1199
1205
|
`Failed to find a free app-server port starting at ${basePort}`
|
|
1200
1206
|
);
|
|
1201
1207
|
}
|
|
1202
|
-
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
1208
|
+
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
|
|
1203
1209
|
const WebSocket = getWebSocketCtor();
|
|
1204
1210
|
if (!WebSocket) {
|
|
1205
1211
|
return false;
|
|
@@ -1221,7 +1227,8 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
|
|
|
1221
1227
|
};
|
|
1222
1228
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
1223
1229
|
try {
|
|
1224
|
-
|
|
1230
|
+
const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
|
|
1231
|
+
socket = new WebSocket(url, protocols);
|
|
1225
1232
|
socket.addEventListener("open", () => finish(true), { once: true });
|
|
1226
1233
|
socket.addEventListener("error", () => finish(false), { once: true });
|
|
1227
1234
|
socket.addEventListener("close", () => finish(false), { once: true });
|
|
@@ -1230,10 +1237,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
|
|
|
1230
1237
|
}
|
|
1231
1238
|
});
|
|
1232
1239
|
}
|
|
1233
|
-
async function waitForAppServerHealth(url, timeoutMs) {
|
|
1240
|
+
async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
|
|
1234
1241
|
const deadline = Date.now() + timeoutMs;
|
|
1235
1242
|
while (Date.now() < deadline) {
|
|
1236
|
-
if (await checkAppServerHealth(
|
|
1243
|
+
if (await checkAppServerHealth(
|
|
1244
|
+
url,
|
|
1245
|
+
APP_SERVER_HEALTH_TIMEOUT_MS,
|
|
1246
|
+
gatewayToken
|
|
1247
|
+
)) {
|
|
1237
1248
|
return true;
|
|
1238
1249
|
}
|
|
1239
1250
|
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
@@ -1498,8 +1509,9 @@ Or start it manually:
|
|
|
1498
1509
|
throw new Error("Tap auth gateway token is missing after startup.");
|
|
1499
1510
|
}
|
|
1500
1511
|
const gatewayHealthy = await waitForAppServerHealth(
|
|
1501
|
-
|
|
1502
|
-
APP_SERVER_GATEWAY_START_TIMEOUT_MS
|
|
1512
|
+
effectiveUrl,
|
|
1513
|
+
APP_SERVER_GATEWAY_START_TIMEOUT_MS,
|
|
1514
|
+
gatewayToken
|
|
1503
1515
|
);
|
|
1504
1516
|
if (!gatewayHealthy) {
|
|
1505
1517
|
await terminateProcess(pid, options.platform);
|
|
@@ -1856,7 +1868,7 @@ function getBridgeStatus(stateDir, instanceId) {
|
|
|
1856
1868
|
}
|
|
1857
1869
|
return "running";
|
|
1858
1870
|
}
|
|
1859
|
-
var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS,
|
|
1871
|
+
var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, AUTH_SUBPROTOCOL_PREFIX, APP_SERVER_AUTH_FILE_MODE;
|
|
1860
1872
|
var init_bridge = __esm({
|
|
1861
1873
|
"src/engine/bridge.ts"() {
|
|
1862
1874
|
"use strict";
|
|
@@ -1868,7 +1880,7 @@ var init_bridge = __esm({
|
|
|
1868
1880
|
APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
1869
1881
|
APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
1870
1882
|
APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
1871
|
-
|
|
1883
|
+
AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
|
|
1872
1884
|
APP_SERVER_AUTH_FILE_MODE = 384;
|
|
1873
1885
|
}
|
|
1874
1886
|
});
|
|
@@ -2496,13 +2508,12 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
2496
2508
|
});
|
|
2497
2509
|
}
|
|
2498
2510
|
if (mainTable && managed.command) {
|
|
2511
|
+
const expectedArgs = managed.args.map((a) => `"${a.replace(/\\/g, "\\\\")}"`).join(", ");
|
|
2499
2512
|
checks.push({
|
|
2500
2513
|
name: "Managed command configured",
|
|
2501
2514
|
passed: mainTable.includes(
|
|
2502
2515
|
`command = "${managed.command.replace(/\\/g, "\\\\")}"`
|
|
2503
|
-
) && mainTable.includes(
|
|
2504
|
-
`args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
|
|
2505
|
-
),
|
|
2516
|
+
) && mainTable.includes(`args = [${expectedArgs}]`),
|
|
2506
2517
|
message: "Managed tap-comms command/args do not match expected values"
|
|
2507
2518
|
});
|
|
2508
2519
|
}
|
|
@@ -3052,11 +3063,11 @@ function redactProtectedUrl(url) {
|
|
|
3052
3063
|
try {
|
|
3053
3064
|
const parsed = new URL(url);
|
|
3054
3065
|
if (parsed.searchParams.has("tap_token")) {
|
|
3055
|
-
parsed.searchParams.
|
|
3066
|
+
parsed.searchParams.delete("tap_token");
|
|
3056
3067
|
}
|
|
3057
3068
|
return parsed.toString().replace(/\/$/, "");
|
|
3058
3069
|
} catch {
|
|
3059
|
-
return url.replace(/tap_token=[^&]+/g, "
|
|
3070
|
+
return url.replace(/[?&]tap_token=[^&]+/g, "");
|
|
3060
3071
|
}
|
|
3061
3072
|
}
|
|
3062
3073
|
function loadCurrentBridgeState(stateDir, instanceId, fallback) {
|
|
@@ -3139,37 +3150,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3139
3150
|
};
|
|
3140
3151
|
}
|
|
3141
3152
|
const instanceId = resolved.instanceId;
|
|
3142
|
-
let
|
|
3143
|
-
if (!
|
|
3153
|
+
let instance2 = state.instances[instanceId];
|
|
3154
|
+
if (!instance2?.installed) {
|
|
3144
3155
|
return {
|
|
3145
3156
|
ok: false,
|
|
3146
3157
|
command: "bridge",
|
|
3147
3158
|
instanceId,
|
|
3148
|
-
runtime:
|
|
3159
|
+
runtime: instance2?.runtime,
|
|
3149
3160
|
code: "TAP_INSTANCE_NOT_FOUND",
|
|
3150
|
-
message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${
|
|
3161
|
+
message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance2?.runtime ?? identifier}`,
|
|
3151
3162
|
warnings: [],
|
|
3152
3163
|
data: {}
|
|
3153
3164
|
};
|
|
3154
3165
|
}
|
|
3155
|
-
const adapter = getAdapter(
|
|
3166
|
+
const adapter = getAdapter(instance2.runtime);
|
|
3156
3167
|
const mode = adapter.bridgeMode();
|
|
3157
3168
|
if (mode !== "app-server") {
|
|
3158
3169
|
return {
|
|
3159
3170
|
ok: true,
|
|
3160
3171
|
command: "bridge",
|
|
3161
3172
|
instanceId,
|
|
3162
|
-
runtime:
|
|
3173
|
+
runtime: instance2.runtime,
|
|
3163
3174
|
code: "TAP_NO_OP",
|
|
3164
3175
|
message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
|
|
3165
3176
|
warnings: [],
|
|
3166
3177
|
data: { bridgeMode: mode }
|
|
3167
3178
|
};
|
|
3168
3179
|
}
|
|
3169
|
-
const resolvedAgentName = agentName ??
|
|
3170
|
-
if (agentName && agentName !==
|
|
3171
|
-
|
|
3172
|
-
const updatedState = updateInstanceState(state, instanceId,
|
|
3180
|
+
const resolvedAgentName = agentName ?? instance2.agentName ?? void 0;
|
|
3181
|
+
if (agentName && agentName !== instance2.agentName) {
|
|
3182
|
+
instance2 = { ...instance2, agentName };
|
|
3183
|
+
const updatedState = updateInstanceState(state, instanceId, instance2);
|
|
3173
3184
|
saveState(repoRoot, updatedState);
|
|
3174
3185
|
state = updatedState;
|
|
3175
3186
|
}
|
|
@@ -3180,7 +3191,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3180
3191
|
ok: false,
|
|
3181
3192
|
command: "bridge",
|
|
3182
3193
|
instanceId,
|
|
3183
|
-
runtime:
|
|
3194
|
+
runtime: instance2.runtime,
|
|
3184
3195
|
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
3185
3196
|
message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
|
|
3186
3197
|
warnings: [],
|
|
@@ -3189,8 +3200,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3189
3200
|
}
|
|
3190
3201
|
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
3191
3202
|
const runtimeCommand = resolvedConfig.runtimeCommand;
|
|
3192
|
-
const manageAppServer =
|
|
3193
|
-
let effectivePort =
|
|
3203
|
+
const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
|
|
3204
|
+
let effectivePort = instance2.port;
|
|
3194
3205
|
if (effectivePort == null && manageAppServer) {
|
|
3195
3206
|
effectivePort = await findNextAvailableAppServerPort(
|
|
3196
3207
|
state,
|
|
@@ -3198,8 +3209,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3198
3209
|
4501,
|
|
3199
3210
|
instanceId
|
|
3200
3211
|
);
|
|
3201
|
-
|
|
3202
|
-
const updatedState = updateInstanceState(state, instanceId,
|
|
3212
|
+
instance2 = { ...instance2, port: effectivePort };
|
|
3213
|
+
const updatedState = updateInstanceState(state, instanceId, instance2);
|
|
3203
3214
|
saveState(repoRoot, updatedState);
|
|
3204
3215
|
state = updatedState;
|
|
3205
3216
|
}
|
|
@@ -3215,19 +3226,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3215
3226
|
if (effectivePort != null) log(`Port: ${effectivePort}`);
|
|
3216
3227
|
if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
|
|
3217
3228
|
const noAuth = flags["no-auth"] === true;
|
|
3218
|
-
if (!manageAppServer &&
|
|
3229
|
+
if (!manageAppServer && instance2.runtime === "codex") {
|
|
3219
3230
|
log("Auto server: disabled (--no-server)");
|
|
3220
3231
|
}
|
|
3221
3232
|
if (noAuth && manageAppServer) {
|
|
3222
3233
|
log("Auth gateway: disabled (--no-auth)");
|
|
3223
3234
|
}
|
|
3224
|
-
const willBeHeadless = flags["headless"] === true ||
|
|
3235
|
+
const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
|
|
3225
3236
|
if (willBeHeadless) {
|
|
3226
|
-
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ??
|
|
3237
|
+
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance2.headless?.role ?? "reviewer";
|
|
3227
3238
|
log(`Headless: ${role}`);
|
|
3228
3239
|
}
|
|
3229
3240
|
try {
|
|
3230
|
-
if (!manageAppServer &&
|
|
3241
|
+
if (!manageAppServer && instance2.runtime === "codex") {
|
|
3231
3242
|
log("Checking app-server health...");
|
|
3232
3243
|
const healthy = await checkAppServerHealth(appServerUrl);
|
|
3233
3244
|
if (healthy) {
|
|
@@ -3238,7 +3249,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3238
3249
|
ok: false,
|
|
3239
3250
|
command: "bridge",
|
|
3240
3251
|
instanceId,
|
|
3241
|
-
runtime:
|
|
3252
|
+
runtime: instance2.runtime,
|
|
3242
3253
|
code: "TAP_BRIDGE_START_FAILED",
|
|
3243
3254
|
message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
|
|
3244
3255
|
warnings: [],
|
|
@@ -3252,7 +3263,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3252
3263
|
ok: false,
|
|
3253
3264
|
command: "bridge",
|
|
3254
3265
|
instanceId,
|
|
3255
|
-
runtime:
|
|
3266
|
+
runtime: instance2.runtime,
|
|
3256
3267
|
code: "TAP_INVALID_ARGUMENT",
|
|
3257
3268
|
message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
|
|
3258
3269
|
warnings: [],
|
|
@@ -3260,9 +3271,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3260
3271
|
};
|
|
3261
3272
|
}
|
|
3262
3273
|
const busyMode = busyModeRaw;
|
|
3263
|
-
const
|
|
3264
|
-
const
|
|
3265
|
-
const
|
|
3274
|
+
const pollSecondsRaw = typeof flags["poll-seconds"] === "string" ? flags["poll-seconds"] : void 0;
|
|
3275
|
+
const reconnectSecondsRaw = typeof flags["reconnect-seconds"] === "string" ? flags["reconnect-seconds"] : void 0;
|
|
3276
|
+
const lookbackRaw = typeof flags["message-lookback-minutes"] === "string" ? flags["message-lookback-minutes"] : void 0;
|
|
3277
|
+
let pollSeconds;
|
|
3278
|
+
let reconnectSeconds;
|
|
3279
|
+
let messageLookbackMinutes;
|
|
3280
|
+
try {
|
|
3281
|
+
pollSeconds = parseIntFlag(pollSecondsRaw, "--poll-seconds", 1, 3600);
|
|
3282
|
+
reconnectSeconds = parseIntFlag(
|
|
3283
|
+
reconnectSecondsRaw,
|
|
3284
|
+
"--reconnect-seconds",
|
|
3285
|
+
1,
|
|
3286
|
+
3600
|
|
3287
|
+
);
|
|
3288
|
+
messageLookbackMinutes = parseIntFlag(
|
|
3289
|
+
lookbackRaw,
|
|
3290
|
+
"--message-lookback-minutes",
|
|
3291
|
+
1,
|
|
3292
|
+
10080
|
|
3293
|
+
);
|
|
3294
|
+
} catch (err) {
|
|
3295
|
+
return {
|
|
3296
|
+
ok: false,
|
|
3297
|
+
command: "bridge",
|
|
3298
|
+
instanceId,
|
|
3299
|
+
runtime: instance2.runtime,
|
|
3300
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
3301
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3302
|
+
warnings: [],
|
|
3303
|
+
data: {}
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3266
3306
|
const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
|
|
3267
3307
|
const ephemeral = flags["ephemeral"] === true;
|
|
3268
3308
|
const processExistingMessages = flags["process-existing-messages"] === true;
|
|
@@ -3274,7 +3314,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3274
3314
|
ok: false,
|
|
3275
3315
|
command: "bridge",
|
|
3276
3316
|
instanceId,
|
|
3277
|
-
runtime:
|
|
3317
|
+
runtime: instance2.runtime,
|
|
3278
3318
|
code: "TAP_INVALID_ARGUMENT",
|
|
3279
3319
|
message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
|
|
3280
3320
|
warnings: [],
|
|
@@ -3286,10 +3326,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3286
3326
|
role: roleArg ?? "reviewer",
|
|
3287
3327
|
maxRounds: 5,
|
|
3288
3328
|
qualitySeverityFloor: "high"
|
|
3289
|
-
} :
|
|
3329
|
+
} : instance2.headless;
|
|
3290
3330
|
const bridge = await startBridge({
|
|
3291
3331
|
instanceId,
|
|
3292
|
-
runtime:
|
|
3332
|
+
runtime: instance2.runtime,
|
|
3293
3333
|
stateDir: ctx.stateDir,
|
|
3294
3334
|
commsDir: ctx.commsDir,
|
|
3295
3335
|
bridgeScript,
|
|
@@ -3330,14 +3370,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3330
3370
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
3331
3371
|
}
|
|
3332
3372
|
}
|
|
3333
|
-
const updated = { ...
|
|
3373
|
+
const updated = { ...instance2, bridge, manageAppServer, noAuth };
|
|
3334
3374
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
3335
3375
|
saveState(repoRoot, newState);
|
|
3336
3376
|
return {
|
|
3337
3377
|
ok: true,
|
|
3338
3378
|
command: "bridge",
|
|
3339
3379
|
instanceId,
|
|
3340
|
-
runtime:
|
|
3380
|
+
runtime: instance2.runtime,
|
|
3341
3381
|
code: "TAP_BRIDGE_START_OK",
|
|
3342
3382
|
message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
|
|
3343
3383
|
warnings: [],
|
|
@@ -3350,7 +3390,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3350
3390
|
ok: false,
|
|
3351
3391
|
command: "bridge",
|
|
3352
3392
|
instanceId,
|
|
3353
|
-
runtime:
|
|
3393
|
+
runtime: instance2.runtime,
|
|
3354
3394
|
code: "TAP_BRIDGE_START_FAILED",
|
|
3355
3395
|
message: msg,
|
|
3356
3396
|
warnings: [],
|
|
@@ -3452,11 +3492,11 @@ async function bridgeStopOne(identifier) {
|
|
|
3452
3492
|
}
|
|
3453
3493
|
const instanceId = resolved.instanceId;
|
|
3454
3494
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
3455
|
-
const
|
|
3495
|
+
const instance2 = state.instances[instanceId];
|
|
3456
3496
|
const bridgeState = loadCurrentBridgeState(
|
|
3457
3497
|
ctx.stateDir,
|
|
3458
3498
|
instanceId,
|
|
3459
|
-
|
|
3499
|
+
instance2?.bridge
|
|
3460
3500
|
);
|
|
3461
3501
|
const appServer = bridgeState?.appServer ?? null;
|
|
3462
3502
|
logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
|
|
@@ -3504,8 +3544,8 @@ async function bridgeStopOne(identifier) {
|
|
|
3504
3544
|
}
|
|
3505
3545
|
}
|
|
3506
3546
|
}
|
|
3507
|
-
if (
|
|
3508
|
-
const updated = { ...
|
|
3547
|
+
if (instance2) {
|
|
3548
|
+
const updated = { ...instance2, bridge: null };
|
|
3509
3549
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
3510
3550
|
saveState(repoRoot, newState);
|
|
3511
3551
|
}
|
|
@@ -3577,9 +3617,9 @@ async function bridgeStopAll() {
|
|
|
3577
3617
|
logSuccess(`Stopped bridge for ${instanceId}`);
|
|
3578
3618
|
stopped.push(instanceId);
|
|
3579
3619
|
}
|
|
3580
|
-
const
|
|
3581
|
-
if (
|
|
3582
|
-
state.instances[instanceId] = { ...
|
|
3620
|
+
const instance2 = state.instances[instanceId];
|
|
3621
|
+
if (instance2?.bridge) {
|
|
3622
|
+
state.instances[instanceId] = { ...instance2, bridge: null };
|
|
3583
3623
|
stateChanged = true;
|
|
3584
3624
|
}
|
|
3585
3625
|
}
|
|
@@ -3875,8 +3915,22 @@ async function bridgeRestart(identifier, flags) {
|
|
|
3875
3915
|
};
|
|
3876
3916
|
}
|
|
3877
3917
|
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
3878
|
-
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] :
|
|
3879
|
-
|
|
3918
|
+
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : void 0;
|
|
3919
|
+
let drainTimeout;
|
|
3920
|
+
try {
|
|
3921
|
+
drainTimeout = parseIntFlag(drainStr, "--drain-timeout", 1, 300) ?? 30;
|
|
3922
|
+
} catch (err) {
|
|
3923
|
+
return {
|
|
3924
|
+
ok: false,
|
|
3925
|
+
command: "bridge",
|
|
3926
|
+
instanceId,
|
|
3927
|
+
runtime: instance.runtime,
|
|
3928
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
3929
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3930
|
+
warnings: [],
|
|
3931
|
+
data: {}
|
|
3932
|
+
};
|
|
3933
|
+
}
|
|
3880
3934
|
logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
|
|
3881
3935
|
log(`Drain timeout: ${drainTimeout}s`);
|
|
3882
3936
|
try {
|
|
@@ -4042,7 +4096,7 @@ Subcommands:
|
|
|
4042
4096
|
|
|
4043
4097
|
Options:
|
|
4044
4098
|
--agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
|
|
4045
|
-
|
|
4099
|
+
Overrides the stored name from 'tap add' when needed
|
|
4046
4100
|
--all Start all registered app-server instances
|
|
4047
4101
|
--busy-mode <steer|wait> How to handle active turns (default: steer)
|
|
4048
4102
|
--poll-seconds <n> Inbox poll interval (default: 5)
|
|
@@ -4349,11 +4403,38 @@ function getConfig(options) {
|
|
|
4349
4403
|
import {
|
|
4350
4404
|
createServer as createServer2
|
|
4351
4405
|
} from "http";
|
|
4406
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
4352
4407
|
var CORS_HEADERS = {
|
|
4353
4408
|
"Access-Control-Allow-Origin": "http://localhost:3000",
|
|
4354
4409
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
4355
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
4410
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
4356
4411
|
};
|
|
4412
|
+
function tokensMatch(presentedToken, expectedToken) {
|
|
4413
|
+
if (!presentedToken) {
|
|
4414
|
+
return false;
|
|
4415
|
+
}
|
|
4416
|
+
const presented = Buffer.from(presentedToken, "utf8");
|
|
4417
|
+
const expected = Buffer.from(expectedToken, "utf8");
|
|
4418
|
+
if (presented.length !== expected.length) {
|
|
4419
|
+
return false;
|
|
4420
|
+
}
|
|
4421
|
+
return timingSafeEqual(presented, expected);
|
|
4422
|
+
}
|
|
4423
|
+
function verifyBearerToken(req, expectedToken) {
|
|
4424
|
+
const header = req.headers.authorization;
|
|
4425
|
+
if (!header?.startsWith("Bearer ")) {
|
|
4426
|
+
return false;
|
|
4427
|
+
}
|
|
4428
|
+
return tokensMatch(header.slice(7), expectedToken);
|
|
4429
|
+
}
|
|
4430
|
+
function verifySseToken(req, expectedToken, serverUrl) {
|
|
4431
|
+
if (verifyBearerToken(req, expectedToken)) {
|
|
4432
|
+
return true;
|
|
4433
|
+
}
|
|
4434
|
+
const url = new URL(req.url ?? "/", serverUrl);
|
|
4435
|
+
const queryToken = url.searchParams.get("token");
|
|
4436
|
+
return tokensMatch(queryToken, expectedToken);
|
|
4437
|
+
}
|
|
4357
4438
|
function jsonResponse(res, data, status = 200) {
|
|
4358
4439
|
res.writeHead(status, {
|
|
4359
4440
|
"Content-Type": "application/json",
|
|
@@ -4396,6 +4477,7 @@ function handleHealth(res, apiOptions) {
|
|
|
4396
4477
|
async function startHttpServer(options) {
|
|
4397
4478
|
const port = options?.port ?? 4580;
|
|
4398
4479
|
const host = "127.0.0.1";
|
|
4480
|
+
const token = options?.token ?? randomBytes2(24).toString("base64url");
|
|
4399
4481
|
const apiOptions = {
|
|
4400
4482
|
repoRoot: options?.repoRoot,
|
|
4401
4483
|
commsDir: options?.commsDir
|
|
@@ -4409,21 +4491,32 @@ async function startHttpServer(options) {
|
|
|
4409
4491
|
res.end();
|
|
4410
4492
|
return;
|
|
4411
4493
|
}
|
|
4494
|
+
if (req.method === "GET" && pathname === "/health") {
|
|
4495
|
+
handleHealth(res, apiOptions);
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
if (req.method === "GET" && pathname === "/api/events") {
|
|
4499
|
+
const serverUrl = `http://${host}:${port}`;
|
|
4500
|
+
if (!verifySseToken(req, token, serverUrl)) {
|
|
4501
|
+
jsonResponse(res, { error: "Unauthorized" }, 401);
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
await handleEvents(req, res, apiOptions);
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
if (!verifyBearerToken(req, token)) {
|
|
4508
|
+
jsonResponse(res, { error: "Unauthorized" }, 401);
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4412
4511
|
try {
|
|
4413
4512
|
if (req.method === "GET") {
|
|
4414
4513
|
switch (pathname) {
|
|
4415
4514
|
case "/api/snapshot":
|
|
4416
4515
|
handleSnapshot(res, apiOptions);
|
|
4417
4516
|
return;
|
|
4418
|
-
case "/api/events":
|
|
4419
|
-
await handleEvents(req, res, apiOptions);
|
|
4420
|
-
return;
|
|
4421
4517
|
case "/api/config":
|
|
4422
4518
|
handleConfig(res, apiOptions);
|
|
4423
4519
|
return;
|
|
4424
|
-
case "/health":
|
|
4425
|
-
handleHealth(res, apiOptions);
|
|
4426
|
-
return;
|
|
4427
4520
|
}
|
|
4428
4521
|
}
|
|
4429
4522
|
if (req.method === "POST") {
|
|
@@ -4461,6 +4554,7 @@ async function startHttpServer(options) {
|
|
|
4461
4554
|
});
|
|
4462
4555
|
return {
|
|
4463
4556
|
port,
|
|
4557
|
+
token,
|
|
4464
4558
|
close: () => new Promise((resolve8, reject) => {
|
|
4465
4559
|
server.close((err) => err ? reject(err) : resolve8());
|
|
4466
4560
|
})
|