@hua-labs/tap 0.5.2 → 0.6.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/AI_GUIDE.md +165 -0
- package/CHANGELOG.md +67 -0
- package/README.md +204 -18
- package/dist/bridges/codex-app-server-auth-gateway.mjs +16 -1
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +105 -12
- package/dist/bridges/codex-app-server-bridge.mjs +3149 -251
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.d.mts +4 -1
- package/dist/bridges/codex-bridge-runner.mjs +512 -58
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/bridges/codex-remote-ipc-relay.d.mts +1 -0
- package/dist/bridges/codex-remote-ipc-relay.mjs +1912 -0
- package/dist/bridges/codex-remote-ipc-relay.mjs.map +1 -0
- package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -1
- package/dist/cli.mjs +30818 -8324
- package/dist/cli.mjs.map +1 -1
- package/dist/codex-a2a/index.d.mts +2 -0
- package/dist/codex-a2a/index.mjs +416 -0
- package/dist/codex-a2a/index.mjs.map +1 -0
- package/dist/codex-health/index.d.mts +76 -0
- package/dist/codex-health/index.mjs +153 -0
- package/dist/codex-health/index.mjs.map +1 -0
- package/dist/codex-ipc/index.d.mts +2 -0
- package/dist/codex-ipc/index.mjs +1834 -0
- package/dist/codex-ipc/index.mjs.map +1 -0
- package/dist/index-D4Khz2Mh.d.mts +206 -0
- package/dist/index-DMToLyGd.d.mts +256 -0
- package/dist/index.d.mts +763 -8
- package/dist/index.mjs +11586 -3438
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +8838 -811
- package/dist/mcp-server.mjs.map +1 -1
- package/dist/types-FWvKrFUt.d.mts +43 -0
- package/examples/01-logic-battle-known-broken.md +46 -0
- package/examples/02-cross-model-review-root-cause.md +37 -0
- package/examples/03-convergence-pattern.md +42 -0
- package/examples/04-tower-broadcast.md +41 -0
- package/examples/05-self-awareness-paradox.md +49 -0
- package/examples/06-session-resurrection.md +37 -0
- package/examples/07-ghost-agent.md +31 -0
- package/examples/08-naming-creates-identity.md +36 -0
- package/examples/09-ceo-as-middleware.md +52 -0
- package/examples/10-files-as-interface.md +67 -0
- package/examples/README.md +34 -0
- package/examples/tap-profile-pack.example.json +71 -0
- package/package.json +21 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/bridges/codex-app-server-bridge.ts
|
|
2
2
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
3
|
-
import { resolve as
|
|
3
|
+
import { basename as basename2, resolve as resolve6 } from "path";
|
|
4
4
|
|
|
5
5
|
// scripts/bridge/bridge-types.ts
|
|
6
6
|
var DEFAULT_AGENT = String.fromCharCode(50728);
|
|
@@ -32,7 +32,7 @@ var STALE_TURN_MS = 5 * 60 * 1e3;
|
|
|
32
32
|
|
|
33
33
|
// scripts/bridge/bridge-routing.ts
|
|
34
34
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
35
|
-
import { join, resolve } from "path";
|
|
35
|
+
import { join, resolve, win32 } from "path";
|
|
36
36
|
|
|
37
37
|
// packages/tap-plugin/channels/tap-identity.ts
|
|
38
38
|
var BROADCAST_RECIPIENTS = /* @__PURE__ */ new Set(["\uC804\uCCB4", "all"]);
|
|
@@ -40,7 +40,7 @@ function trimAddress(value) {
|
|
|
40
40
|
return value?.trim() ?? "";
|
|
41
41
|
}
|
|
42
42
|
function canonicalizeAgentId(value) {
|
|
43
|
-
return trimAddress(value).replace(/-/g, "_");
|
|
43
|
+
return trimAddress(value).replace(/-/g, "_").toLowerCase();
|
|
44
44
|
}
|
|
45
45
|
function isBroadcastRecipient(value) {
|
|
46
46
|
return BROADCAST_RECIPIENTS.has(trimAddress(value));
|
|
@@ -75,8 +75,33 @@ function isOwnMessageAddress(sender, agentId, agentName) {
|
|
|
75
75
|
function canonicalize(id) {
|
|
76
76
|
return canonicalizeAgentId(id);
|
|
77
77
|
}
|
|
78
|
+
var WINDOWS_NAMESPACE_PREFIX = "\\\\?\\";
|
|
79
|
+
var WINDOWS_NAMESPACE_UNC_PREFIX = "\\\\?\\UNC\\";
|
|
80
|
+
function looksLikeWindowsAbsolutePath(value) {
|
|
81
|
+
return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
|
|
82
|
+
}
|
|
83
|
+
function stripWindowsNamespacePrefix(cwd) {
|
|
84
|
+
const trimmed = cwd.trim();
|
|
85
|
+
if (trimmed.startsWith(WINDOWS_NAMESPACE_UNC_PREFIX)) {
|
|
86
|
+
return `\\\\${trimmed.slice(WINDOWS_NAMESPACE_UNC_PREFIX.length)}`;
|
|
87
|
+
}
|
|
88
|
+
if (trimmed.startsWith(WINDOWS_NAMESPACE_PREFIX)) {
|
|
89
|
+
return trimmed.slice(WINDOWS_NAMESPACE_PREFIX.length);
|
|
90
|
+
}
|
|
91
|
+
return trimmed;
|
|
92
|
+
}
|
|
93
|
+
function resolveThreadCwd(cwd) {
|
|
94
|
+
const normalized = stripWindowsNamespacePrefix(cwd);
|
|
95
|
+
return looksLikeWindowsAbsolutePath(normalized) ? win32.resolve(normalized) : resolve(normalized);
|
|
96
|
+
}
|
|
78
97
|
function normalizeThreadCwd(cwd) {
|
|
79
|
-
return
|
|
98
|
+
return resolveThreadCwd(cwd).replace(/\\/g, "/").toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
function normalizePersistedThreadCwd(cwd) {
|
|
101
|
+
if (!cwd?.trim()) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return resolveThreadCwd(cwd);
|
|
80
105
|
}
|
|
81
106
|
function threadCwdMatches(expectedCwd, actualCwd) {
|
|
82
107
|
if (!actualCwd) {
|
|
@@ -85,24 +110,33 @@ function threadCwdMatches(expectedCwd, actualCwd) {
|
|
|
85
110
|
return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
|
|
86
111
|
}
|
|
87
112
|
function chooseLoadedThreadForCwd(cwd, threads) {
|
|
88
|
-
const
|
|
113
|
+
const reusable = threads.filter((thread) => {
|
|
89
114
|
if (!threadCwdMatches(cwd, thread.cwd)) {
|
|
90
115
|
return false;
|
|
91
116
|
}
|
|
92
|
-
|
|
117
|
+
if (thread.statusType === "notLoaded") {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (thread.statusType === "active") {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const threadActiveFlags = Array.isArray(
|
|
124
|
+
thread.thread?.status?.activeFlags
|
|
125
|
+
) ? thread.thread.status.activeFlags : [];
|
|
126
|
+
if (isTurnStuckOnApproval(threadActiveFlags)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const turns = Array.isArray(thread.thread?.turns) ? thread.thread.turns : [];
|
|
130
|
+
return !turns.some((turn) => {
|
|
131
|
+
const activeFlags = Array.isArray(turn?.activeFlags) ? turn.activeFlags : [];
|
|
132
|
+
return turn?.status === "inProgress" && isTurnStuckOnApproval(activeFlags);
|
|
133
|
+
});
|
|
93
134
|
});
|
|
94
|
-
if (
|
|
135
|
+
if (reusable.length === 0) {
|
|
95
136
|
return null;
|
|
96
137
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const rightActive = right.statusType === "active" ? 1 : 0;
|
|
100
|
-
if (leftActive !== rightActive) {
|
|
101
|
-
return rightActive - leftActive;
|
|
102
|
-
}
|
|
103
|
-
return right.updatedAt - left.updatedAt;
|
|
104
|
-
});
|
|
105
|
-
return matching[0] ?? null;
|
|
138
|
+
reusable.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
139
|
+
return reusable[0] ?? null;
|
|
106
140
|
}
|
|
107
141
|
function normalizeAgentToken(value) {
|
|
108
142
|
const normalized = value?.trim();
|
|
@@ -190,6 +224,12 @@ function isOwnMessageSender(sender, agentId, agentName) {
|
|
|
190
224
|
function isTurnStuckOnApproval(activeFlags) {
|
|
191
225
|
return activeFlags.includes("waitingOnApproval");
|
|
192
226
|
}
|
|
227
|
+
function isWaitingApprovalStatus(status) {
|
|
228
|
+
if (!status) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return /approval|input-required|confirm|consent/i.test(status);
|
|
232
|
+
}
|
|
193
233
|
function isTurnStale(turnStartedAt, nowMs = Date.now()) {
|
|
194
234
|
if (!turnStartedAt) return false;
|
|
195
235
|
return nowMs - new Date(turnStartedAt).getTime() > STALE_TURN_MS;
|
|
@@ -201,6 +241,81 @@ function shouldRetrySteerAsStart(error) {
|
|
|
201
241
|
const message = error.message.toLowerCase();
|
|
202
242
|
return message.includes("no active turn") || message.includes("expectedturnid") || message.includes("turn/steer failed") && (message.includes("active turn") || message.includes("not found"));
|
|
203
243
|
}
|
|
244
|
+
var FORBIDDEN_RAW_PAIR_TOKEN_REASON = "envelope rejected: forbidden raw pairToken field present (M355 defensive drop)";
|
|
245
|
+
function normalizeFrontmatterValue(value) {
|
|
246
|
+
const normalized = value?.trim();
|
|
247
|
+
return normalized ? normalized : null;
|
|
248
|
+
}
|
|
249
|
+
function parseJsonObject(value) {
|
|
250
|
+
const normalized = normalizeFrontmatterValue(value);
|
|
251
|
+
if (!normalized) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(normalized);
|
|
256
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return parsed;
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function parseFrontmatterScope(value) {
|
|
265
|
+
const normalized = normalizeFrontmatterValue(value);
|
|
266
|
+
if (normalized === "observe" || normalized === "suggest" || normalized === "drive") {
|
|
267
|
+
return normalized;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function parseFrontmatterAliases(value) {
|
|
272
|
+
if (!Array.isArray(value)) {
|
|
273
|
+
return void 0;
|
|
274
|
+
}
|
|
275
|
+
const aliases = [];
|
|
276
|
+
for (const candidate of value) {
|
|
277
|
+
if (typeof candidate !== "string") continue;
|
|
278
|
+
const normalized = candidate.trim();
|
|
279
|
+
if (!normalized || aliases.includes(normalized)) continue;
|
|
280
|
+
aliases.push(normalized);
|
|
281
|
+
}
|
|
282
|
+
return aliases.length > 0 ? aliases : void 0;
|
|
283
|
+
}
|
|
284
|
+
function parseFrontmatterSlot(value) {
|
|
285
|
+
if (typeof value !== "string") {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const normalized = value.trim();
|
|
289
|
+
if (normalized === "tower" || normalized === "reviewer" || /^wt-\d+$/.test(normalized)) {
|
|
290
|
+
return normalized;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function parseFrontmatterAddress(value) {
|
|
295
|
+
const record = parseJsonObject(value);
|
|
296
|
+
if (!record) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
hostId: normalizeFrontmatterValue(
|
|
301
|
+
typeof record.hostId === "string" ? record.hostId : void 0
|
|
302
|
+
),
|
|
303
|
+
clientId: normalizeFrontmatterValue(
|
|
304
|
+
typeof record.clientId === "string" ? record.clientId : void 0
|
|
305
|
+
),
|
|
306
|
+
conversationId: normalizeFrontmatterValue(
|
|
307
|
+
typeof record.conversationId === "string" ? record.conversationId : void 0
|
|
308
|
+
),
|
|
309
|
+
ownerClientId: normalizeFrontmatterValue(
|
|
310
|
+
typeof record.ownerClientId === "string" ? record.ownerClientId : void 0
|
|
311
|
+
),
|
|
312
|
+
routingAddress: normalizeFrontmatterValue(
|
|
313
|
+
typeof record.routingAddress === "string" ? record.routingAddress : void 0
|
|
314
|
+
) ?? void 0,
|
|
315
|
+
slot: parseFrontmatterSlot(record.slot),
|
|
316
|
+
aliases: parseFrontmatterAliases(record.aliases)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
204
319
|
function parseBridgeFrontmatter(content) {
|
|
205
320
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
206
321
|
if (!match) return null;
|
|
@@ -213,7 +328,14 @@ function parseBridgeFrontmatter(content) {
|
|
|
213
328
|
return {
|
|
214
329
|
sender: fields.from,
|
|
215
330
|
recipient: fields.to,
|
|
216
|
-
subject: fields.subject ?? ""
|
|
331
|
+
subject: fields.subject ?? "",
|
|
332
|
+
messageId: normalizeFrontmatterValue(fields.message_id) ?? normalizeFrontmatterValue(fields.messageId),
|
|
333
|
+
fromAddress: parseFrontmatterAddress(fields.from_address),
|
|
334
|
+
toAddress: parseFrontmatterAddress(fields.to_address),
|
|
335
|
+
scope: parseFrontmatterScope(fields.scope),
|
|
336
|
+
action: normalizeFrontmatterValue(fields.action),
|
|
337
|
+
consentRef: normalizeFrontmatterValue(fields.consent_ref) ?? normalizeFrontmatterValue(fields.consentRef),
|
|
338
|
+
validationError: normalizeFrontmatterValue(fields.pairToken) ?? normalizeFrontmatterValue(fields.pair_token) ? FORBIDDEN_RAW_PAIR_TOKEN_REASON : null
|
|
217
339
|
};
|
|
218
340
|
}
|
|
219
341
|
function stripBridgeFrontmatter(content) {
|
|
@@ -236,23 +358,20 @@ function getInboxRouteFromFilename(fileName) {
|
|
|
236
358
|
return {
|
|
237
359
|
sender: parts[offset] ?? "",
|
|
238
360
|
recipient: parts[offset + 1] ?? "",
|
|
239
|
-
subject: parts.slice(offset + 2).join("-")
|
|
361
|
+
subject: parts.slice(offset + 2).join("-"),
|
|
362
|
+
validationError: null
|
|
240
363
|
};
|
|
241
364
|
}
|
|
242
365
|
|
|
243
366
|
// scripts/bridge/bridge-config.ts
|
|
244
|
-
import { existsSync as
|
|
245
|
-
import { isAbsolute
|
|
246
|
-
|
|
247
|
-
// src/config/resolve.ts
|
|
248
|
-
import * as fs from "fs";
|
|
249
|
-
import * as path from "path";
|
|
250
|
-
function normalizeTapPath(input) {
|
|
367
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "fs";
|
|
368
|
+
import { isAbsolute, join as join2, resolve as resolve2 } from "path";
|
|
369
|
+
function normalizeTapPath(input, platform = process.platform) {
|
|
251
370
|
const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
252
371
|
if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
253
372
|
return trimmed;
|
|
254
373
|
}
|
|
255
|
-
if (
|
|
374
|
+
if (platform === "win32") {
|
|
256
375
|
const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
|
|
257
376
|
if (match) {
|
|
258
377
|
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
@@ -260,19 +379,17 @@ function normalizeTapPath(input) {
|
|
|
260
379
|
}
|
|
261
380
|
return trimmed;
|
|
262
381
|
}
|
|
263
|
-
|
|
264
|
-
// scripts/bridge/bridge-config.ts
|
|
265
382
|
function ensureDir(target) {
|
|
266
|
-
if (!
|
|
383
|
+
if (!existsSync2(target)) {
|
|
267
384
|
mkdirSync(target, { recursive: true });
|
|
268
385
|
}
|
|
269
|
-
return
|
|
386
|
+
return resolve2(target);
|
|
270
387
|
}
|
|
271
388
|
function printHelp() {
|
|
272
389
|
console.log(`Codex App Server bridge
|
|
273
390
|
|
|
274
391
|
Usage:
|
|
275
|
-
node --experimental-strip-types scripts/codex-app-server-bridge.ts [options]
|
|
392
|
+
node --experimental-strip-types scripts/codex/codex-app-server-bridge.ts [options]
|
|
276
393
|
|
|
277
394
|
Options:
|
|
278
395
|
--repo-root=<path>
|
|
@@ -465,29 +582,33 @@ function parseArgs(argv) {
|
|
|
465
582
|
}
|
|
466
583
|
function resolveRepoRoot(explicit) {
|
|
467
584
|
if (explicit) {
|
|
468
|
-
return
|
|
585
|
+
return resolve2(explicit);
|
|
469
586
|
}
|
|
470
587
|
return process.cwd();
|
|
471
588
|
}
|
|
472
589
|
function resolveTapConfigPath(repoRoot, input) {
|
|
473
590
|
const converted = normalizeTapPath(input);
|
|
474
|
-
return
|
|
591
|
+
return isAbsolute(converted) ? resolve2(converted) : resolve2(repoRoot, converted);
|
|
475
592
|
}
|
|
476
593
|
function resolveCommsDir(repoRoot, explicit) {
|
|
477
594
|
if (explicit) {
|
|
478
|
-
return
|
|
595
|
+
return resolve2(normalizeTapPath(explicit));
|
|
596
|
+
}
|
|
597
|
+
const envCommsDir = process.env.TAP_COMMS_DIR?.trim();
|
|
598
|
+
if (envCommsDir) {
|
|
599
|
+
return resolveTapConfigPath(repoRoot, envCommsDir);
|
|
479
600
|
}
|
|
480
|
-
const tapConfigPath =
|
|
481
|
-
if (!
|
|
601
|
+
const tapConfigPath = join2(repoRoot, ".tap-config");
|
|
602
|
+
if (!existsSync2(tapConfigPath)) {
|
|
482
603
|
throw new Error(
|
|
483
|
-
"Unable to resolve comms directory. Pass --comms-dir
|
|
604
|
+
"Unable to resolve comms directory. Pass --comms-dir or set TAP_COMMS_DIR."
|
|
484
605
|
);
|
|
485
606
|
}
|
|
486
|
-
const configText =
|
|
607
|
+
const configText = readFileSync2(tapConfigPath, "utf8");
|
|
487
608
|
const match = configText.match(/^TAP_COMMS_DIR="?(.*?)"?$/m);
|
|
488
609
|
if (!match?.[1]) {
|
|
489
610
|
throw new Error(
|
|
490
|
-
"Unable to resolve comms directory. Pass --comms-dir
|
|
611
|
+
"Unable to resolve comms directory. Pass --comms-dir or set TAP_COMMS_DIR."
|
|
491
612
|
);
|
|
492
613
|
}
|
|
493
614
|
return resolveTapConfigPath(repoRoot, match[1]);
|
|
@@ -510,22 +631,33 @@ function sanitizeStateSegment(agentName) {
|
|
|
510
631
|
}
|
|
511
632
|
function buildDefaultStateDir(repoRoot, preferredAgentName) {
|
|
512
633
|
const suffix = preferredAgentName?.trim() ? `-${sanitizeStateSegment(preferredAgentName)}` : "";
|
|
513
|
-
return
|
|
634
|
+
return resolve2(join2(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
|
|
514
635
|
}
|
|
515
636
|
function resolveStateDir(repoRoot, explicit, preferredAgentName) {
|
|
516
|
-
const root = explicit ?
|
|
637
|
+
const root = explicit ? resolve2(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
|
|
517
638
|
ensureDir(root);
|
|
518
|
-
ensureDir(
|
|
519
|
-
ensureDir(
|
|
639
|
+
ensureDir(join2(root, "processed"));
|
|
640
|
+
ensureDir(join2(root, "logs"));
|
|
520
641
|
return root;
|
|
521
642
|
}
|
|
522
643
|
function readGatewayTokenFile(tokenFile) {
|
|
523
|
-
const token =
|
|
644
|
+
const token = readFileSync2(tokenFile, "utf8").trim();
|
|
524
645
|
if (!token) {
|
|
525
646
|
throw new Error(`Gateway token file is empty: ${tokenFile}`);
|
|
526
647
|
}
|
|
527
648
|
return token;
|
|
528
649
|
}
|
|
650
|
+
function normalizeRoutingSlotEnv(value) {
|
|
651
|
+
const normalized = value?.trim().toLowerCase();
|
|
652
|
+
if (!normalized) return null;
|
|
653
|
+
if (normalized === "tower") return "tower";
|
|
654
|
+
if (normalized === "reviewer") return "reviewer";
|
|
655
|
+
const worktreeMatch = normalized.match(/^wt[-_]?(\d+)$/);
|
|
656
|
+
if (worktreeMatch) {
|
|
657
|
+
return `wt-${Number.parseInt(worktreeMatch[1], 10)}`;
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
529
661
|
function buildOptions(argv) {
|
|
530
662
|
const parsed = parseArgs(argv);
|
|
531
663
|
const repoRoot = resolveRepoRoot(parsed.repoRoot);
|
|
@@ -541,6 +673,7 @@ function buildOptions(argv) {
|
|
|
541
673
|
persistAgentName(stateDir, agentName);
|
|
542
674
|
const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
|
|
543
675
|
const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
|
|
676
|
+
const routingSlot = normalizeRoutingSlotEnv(process.env.TAP_ROUTING_SLOT);
|
|
544
677
|
return {
|
|
545
678
|
repoRoot,
|
|
546
679
|
commsDir,
|
|
@@ -561,13 +694,20 @@ function buildOptions(argv) {
|
|
|
561
694
|
busyMode: parsed.busyMode ?? "steer",
|
|
562
695
|
logLevel: parsed.logLevel ?? "info",
|
|
563
696
|
threadId: parsed.threadId?.trim() || null,
|
|
564
|
-
ephemeral: parsed.ephemeral
|
|
697
|
+
ephemeral: parsed.ephemeral,
|
|
698
|
+
routingSlot
|
|
565
699
|
};
|
|
566
700
|
}
|
|
567
701
|
|
|
568
702
|
// scripts/bridge/bridge-candidates.ts
|
|
569
703
|
import { createHash } from "crypto";
|
|
570
|
-
import {
|
|
704
|
+
import {
|
|
705
|
+
existsSync as existsSync3,
|
|
706
|
+
readFileSync as readFileSync3,
|
|
707
|
+
readdirSync,
|
|
708
|
+
statSync,
|
|
709
|
+
unlinkSync
|
|
710
|
+
} from "fs";
|
|
571
711
|
import { join as join4 } from "path";
|
|
572
712
|
|
|
573
713
|
// scripts/bridge/bridge-logging.ts
|
|
@@ -642,28 +782,192 @@ function createBridgeLogger(scope) {
|
|
|
642
782
|
};
|
|
643
783
|
}
|
|
644
784
|
|
|
645
|
-
// scripts/bridge/bridge-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
785
|
+
// scripts/bridge/bridge-format.ts
|
|
786
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
787
|
+
import { join as join3 } from "path";
|
|
788
|
+
|
|
789
|
+
// src/routing/tap-message-prompt.ts
|
|
790
|
+
function isValidReplyTarget(value) {
|
|
791
|
+
const normalized = value?.trim().toLowerCase();
|
|
792
|
+
return Boolean(
|
|
793
|
+
normalized && normalized !== "unknown" && normalized !== "unnamed" && normalized !== "null" && normalized !== "undefined" && normalized !== "?"
|
|
794
|
+
);
|
|
649
795
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
796
|
+
function resolveReplyTarget(options) {
|
|
797
|
+
if (isValidReplyTarget(options.returnAddress?.routingAddress)) {
|
|
798
|
+
return options.returnAddress.routingAddress.trim();
|
|
799
|
+
}
|
|
800
|
+
if (isValidReplyTarget(options.replyTo)) {
|
|
801
|
+
return options.replyTo.trim();
|
|
802
|
+
}
|
|
803
|
+
return null;
|
|
652
804
|
}
|
|
653
|
-
function
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
805
|
+
function formatReturnRoute(options) {
|
|
806
|
+
const address = options.returnAddress;
|
|
807
|
+
const parts = [];
|
|
808
|
+
if (isValidReplyTarget(address?.routingAddress)) {
|
|
809
|
+
parts.push(`routingAddress=${address.routingAddress.trim()}`);
|
|
810
|
+
}
|
|
811
|
+
if (address?.hostId?.trim()) parts.push(`hostId=${address.hostId.trim()}`);
|
|
812
|
+
if (options.runtimeSurface?.trim()) {
|
|
813
|
+
parts.push(`runtimeSurface=${options.runtimeSurface.trim()}`);
|
|
658
814
|
}
|
|
815
|
+
if (address?.clientId?.trim()) {
|
|
816
|
+
parts.push(`clientId=${address.clientId.trim()}`);
|
|
817
|
+
}
|
|
818
|
+
if (address?.conversationId?.trim()) {
|
|
819
|
+
parts.push(`conversationId=${address.conversationId.trim()}`);
|
|
820
|
+
}
|
|
821
|
+
if (address?.ownerClientId?.trim()) {
|
|
822
|
+
parts.push(`ownerClientId=${address.ownerClientId.trim()}`);
|
|
823
|
+
}
|
|
824
|
+
if (address?.surfaceInstanceId?.trim()) {
|
|
825
|
+
parts.push(`surfaceInstanceId=${address.surfaceInstanceId.trim()}`);
|
|
826
|
+
}
|
|
827
|
+
return parts.length ? parts.join("; ") : null;
|
|
659
828
|
}
|
|
660
|
-
function
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
return
|
|
829
|
+
function createTapMessageViewModel(options) {
|
|
830
|
+
const body = options.body.trim();
|
|
831
|
+
const replyTo = resolveReplyTarget(options);
|
|
832
|
+
const returnRoute = formatReturnRoute(options);
|
|
833
|
+
return {
|
|
834
|
+
agentName: options.agentName,
|
|
835
|
+
sender: options.sender,
|
|
836
|
+
recipient: options.recipient,
|
|
837
|
+
subject: options.subject,
|
|
838
|
+
body: body || "(empty)",
|
|
839
|
+
replyTarget: replyTo,
|
|
840
|
+
returnRoute,
|
|
841
|
+
missingRoute: !replyTo,
|
|
842
|
+
debugEnvelope: {
|
|
843
|
+
fileName: options.fileName,
|
|
844
|
+
returnAddress: options.returnAddress ?? null,
|
|
845
|
+
runtimeSurface: options.runtimeSurface ?? null
|
|
846
|
+
}
|
|
847
|
+
};
|
|
665
848
|
}
|
|
666
|
-
function
|
|
849
|
+
function renderDebugEnvelope(viewModel) {
|
|
850
|
+
const address = viewModel.debugEnvelope.returnAddress;
|
|
851
|
+
const lines = [
|
|
852
|
+
"",
|
|
853
|
+
"Debug envelope:",
|
|
854
|
+
`- file: ${viewModel.debugEnvelope.fileName}`
|
|
855
|
+
];
|
|
856
|
+
if (viewModel.replyTarget) {
|
|
857
|
+
lines.push(
|
|
858
|
+
`- replyInstruction: Use tap_reply(to: "${viewModel.replyTarget}", subject: "<your-subject>", content: "<your-response>").`
|
|
859
|
+
);
|
|
860
|
+
} else {
|
|
861
|
+
lines.push("- replyInstruction: unavailable; do not reply to unknown");
|
|
862
|
+
}
|
|
863
|
+
if (viewModel.returnRoute) {
|
|
864
|
+
lines.push(`- returnRoute: ${viewModel.returnRoute}`);
|
|
865
|
+
}
|
|
866
|
+
if (viewModel.debugEnvelope.runtimeSurface?.trim()) {
|
|
867
|
+
lines.push(
|
|
868
|
+
`- runtimeSurface: ${viewModel.debugEnvelope.runtimeSurface.trim()}`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
if (address?.aliases?.length) {
|
|
872
|
+
lines.push(`- aliases: ${address.aliases.join(", ")}`);
|
|
873
|
+
}
|
|
874
|
+
return lines;
|
|
875
|
+
}
|
|
876
|
+
function renderAgentMessagePrompt(viewModel, options = {}) {
|
|
877
|
+
const replyInstructions = viewModel.replyTarget ? ["Reply:", `Reply available: ${viewModel.replyTarget}`] : [
|
|
878
|
+
"Reply:",
|
|
879
|
+
"Reply unavailable: no verified return route.",
|
|
880
|
+
"No valid structured return route was provided; `unknown` is not a valid reply target.",
|
|
881
|
+
"Preserve durable inbox evidence or ask tower/operator for a valid return route before replying.",
|
|
882
|
+
"If the message is a review request, perform the review locally and report that the return route is missing.",
|
|
883
|
+
'Do not reply to "unknown".'
|
|
884
|
+
];
|
|
885
|
+
const lines = [
|
|
886
|
+
`Tap message for ${viewModel.agentName}`,
|
|
887
|
+
`From: ${viewModel.sender}`,
|
|
888
|
+
`To: ${viewModel.recipient}`,
|
|
889
|
+
`Subject: ${viewModel.subject}`,
|
|
890
|
+
"",
|
|
891
|
+
"Message:",
|
|
892
|
+
viewModel.body,
|
|
893
|
+
"",
|
|
894
|
+
...replyInstructions
|
|
895
|
+
];
|
|
896
|
+
if (options.debugEnvelope) {
|
|
897
|
+
lines.push(...renderDebugEnvelope(viewModel));
|
|
898
|
+
}
|
|
899
|
+
return lines.join("\n");
|
|
900
|
+
}
|
|
901
|
+
function buildTapMessagePrompt(options) {
|
|
902
|
+
return renderAgentMessagePrompt(createTapMessageViewModel(options), {
|
|
903
|
+
debugEnvelope: options.debugEnvelope
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// scripts/bridge/bridge-format.ts
|
|
908
|
+
function buildUserInput(candidate, agentName, heartbeats) {
|
|
909
|
+
const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
|
|
910
|
+
const recipient = resolveAddressLabel(
|
|
911
|
+
candidate.recipient || agentName,
|
|
912
|
+
heartbeats
|
|
913
|
+
);
|
|
914
|
+
const subject = candidate.subject || "(none)";
|
|
915
|
+
return buildTapMessagePrompt({
|
|
916
|
+
agentName,
|
|
917
|
+
sender,
|
|
918
|
+
recipient,
|
|
919
|
+
subject,
|
|
920
|
+
fileName: candidate.fileName,
|
|
921
|
+
body: candidate.body,
|
|
922
|
+
replyTo: candidate.sender || "unknown",
|
|
923
|
+
returnAddress: candidate.fromAddress
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnId, blockedReason) {
|
|
927
|
+
const payload = {
|
|
928
|
+
requestFile: candidate.filePath,
|
|
929
|
+
requestName: candidate.fileName,
|
|
930
|
+
sender: candidate.sender,
|
|
931
|
+
recipient: candidate.recipient,
|
|
932
|
+
subject: candidate.subject,
|
|
933
|
+
dispatchMode,
|
|
934
|
+
threadId,
|
|
935
|
+
turnId,
|
|
936
|
+
blockedReason: blockedReason?.trim() || null,
|
|
937
|
+
markedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
938
|
+
};
|
|
939
|
+
writeFileSync2(
|
|
940
|
+
getProcessedMarkerPath(stateDir, candidate.markerId),
|
|
941
|
+
`${JSON.stringify(payload, null, 2)}
|
|
942
|
+
`,
|
|
943
|
+
"utf8"
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId, blockedReason) {
|
|
947
|
+
const payload = {
|
|
948
|
+
requestFile: candidate.filePath,
|
|
949
|
+
requestName: candidate.fileName,
|
|
950
|
+
markerId: candidate.markerId,
|
|
951
|
+
sender: candidate.sender,
|
|
952
|
+
recipient: candidate.recipient,
|
|
953
|
+
subject: candidate.subject,
|
|
954
|
+
dispatchMode,
|
|
955
|
+
threadId,
|
|
956
|
+
turnId,
|
|
957
|
+
blockedReason: blockedReason?.trim() || null,
|
|
958
|
+
dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
959
|
+
};
|
|
960
|
+
writeFileSync2(
|
|
961
|
+
join3(stateDir, "last-dispatch.json"),
|
|
962
|
+
`${JSON.stringify(payload, null, 2)}
|
|
963
|
+
`,
|
|
964
|
+
"utf8"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// scripts/bridge/bridge-candidates.ts
|
|
969
|
+
var routingLogger = createBridgeLogger("routing");
|
|
970
|
+
function scanCandidates(inboxDir, agentId, agentName, aliasName) {
|
|
667
971
|
const entries = readdirSync(inboxDir, { withFileTypes: true }).filter(
|
|
668
972
|
(entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")
|
|
669
973
|
).map((entry) => {
|
|
@@ -672,13 +976,14 @@ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
|
|
|
672
976
|
return { entry, filePath, stats };
|
|
673
977
|
}).sort((left, right) => left.stats.mtimeMs - right.stats.mtimeMs);
|
|
674
978
|
const candidates = [];
|
|
979
|
+
const rejected = [];
|
|
675
980
|
let filteredByRecipient = 0;
|
|
676
981
|
let filteredBySelf = 0;
|
|
677
982
|
let filteredByHeadless = 0;
|
|
678
983
|
for (const item of entries) {
|
|
679
984
|
let body;
|
|
680
985
|
try {
|
|
681
|
-
body =
|
|
986
|
+
body = readFileSync3(item.filePath, "utf8");
|
|
682
987
|
} catch {
|
|
683
988
|
continue;
|
|
684
989
|
}
|
|
@@ -695,21 +1000,42 @@ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
|
|
|
695
1000
|
filteredByHeadless += 1;
|
|
696
1001
|
continue;
|
|
697
1002
|
}
|
|
1003
|
+
const markerId = buildMarkerId(item.filePath, item.stats.mtimeMs);
|
|
1004
|
+
if (route.validationError) {
|
|
1005
|
+
rejected.push({
|
|
1006
|
+
markerId,
|
|
1007
|
+
filePath: item.filePath,
|
|
1008
|
+
fileName: item.entry.name,
|
|
1009
|
+
sender: route.sender,
|
|
1010
|
+
recipient: route.recipient,
|
|
1011
|
+
subject: route.subject,
|
|
1012
|
+
mtimeMs: item.stats.mtimeMs,
|
|
1013
|
+
rejectionReason: route.validationError
|
|
1014
|
+
});
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
698
1017
|
candidates.push({
|
|
699
|
-
markerId
|
|
1018
|
+
markerId,
|
|
700
1019
|
filePath: item.filePath,
|
|
701
1020
|
fileName: item.entry.name,
|
|
702
1021
|
sender: route.sender,
|
|
703
1022
|
recipient: route.recipient,
|
|
704
1023
|
subject: route.subject,
|
|
705
1024
|
body: stripBridgeFrontmatter(body),
|
|
706
|
-
mtimeMs: item.stats.mtimeMs
|
|
1025
|
+
mtimeMs: item.stats.mtimeMs,
|
|
1026
|
+
messageId: route.messageId ?? null,
|
|
1027
|
+
fromAddress: route.fromAddress ?? null,
|
|
1028
|
+
toAddress: route.toAddress ?? null,
|
|
1029
|
+
scope: route.scope ?? null,
|
|
1030
|
+
action: route.action ?? null,
|
|
1031
|
+
consentRef: route.consentRef ?? null
|
|
707
1032
|
});
|
|
708
1033
|
}
|
|
709
1034
|
routingLogger.debug("candidate scan completed", {
|
|
710
1035
|
inboxDir,
|
|
711
1036
|
scanned: entries.length,
|
|
712
1037
|
matched: candidates.length,
|
|
1038
|
+
rejected: rejected.length,
|
|
713
1039
|
filteredByRecipient,
|
|
714
1040
|
filteredBySelf,
|
|
715
1041
|
filteredByHeadless,
|
|
@@ -717,27 +1043,148 @@ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
|
|
|
717
1043
|
agentName,
|
|
718
1044
|
aliasName
|
|
719
1045
|
});
|
|
720
|
-
return candidates;
|
|
1046
|
+
return { candidates, rejected };
|
|
1047
|
+
}
|
|
1048
|
+
function buildMarkerId(filePath, mtimeMs) {
|
|
1049
|
+
return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
|
|
1050
|
+
}
|
|
1051
|
+
function getProcessedMarkerPath(stateDir, markerId) {
|
|
1052
|
+
return join4(stateDir, "processed", `${markerId}.done`);
|
|
1053
|
+
}
|
|
1054
|
+
var PROCESSED_MARKER_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
1055
|
+
var PROCESSED_MARKER_GRACE_MS = 1e4;
|
|
1056
|
+
function sweepOrphanProcessedMarkers(stateDir, options) {
|
|
1057
|
+
const result = {
|
|
1058
|
+
scanned: 0,
|
|
1059
|
+
removed: 0,
|
|
1060
|
+
kept: 0,
|
|
1061
|
+
errors: 0,
|
|
1062
|
+
removedMarkerIds: []
|
|
1063
|
+
};
|
|
1064
|
+
const dir = join4(stateDir, "processed");
|
|
1065
|
+
if (!existsSync3(dir)) {
|
|
1066
|
+
return result;
|
|
1067
|
+
}
|
|
1068
|
+
const now = options?.nowMs ?? Date.now();
|
|
1069
|
+
const maxAge = options?.maxAgeMs ?? PROCESSED_MARKER_MAX_AGE_MS;
|
|
1070
|
+
const grace = options?.graceMs ?? PROCESSED_MARKER_GRACE_MS;
|
|
1071
|
+
const log = options?.logger ?? (() => void 0);
|
|
1072
|
+
let entries;
|
|
1073
|
+
try {
|
|
1074
|
+
entries = readdirSync(dir);
|
|
1075
|
+
} catch {
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1078
|
+
for (const file of entries) {
|
|
1079
|
+
if (!file.endsWith(".done")) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
result.scanned += 1;
|
|
1083
|
+
const markerPath = join4(dir, file);
|
|
1084
|
+
let markerMtimeMs = 0;
|
|
1085
|
+
let sourcePath = null;
|
|
1086
|
+
try {
|
|
1087
|
+
markerMtimeMs = statSync(markerPath).mtimeMs;
|
|
1088
|
+
const payload = JSON.parse(readFileSync3(markerPath, "utf8"));
|
|
1089
|
+
sourcePath = typeof payload.requestFile === "string" && payload.requestFile.trim() ? payload.requestFile : null;
|
|
1090
|
+
} catch {
|
|
1091
|
+
result.errors += 1;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const ageMs = now - markerMtimeMs;
|
|
1095
|
+
if (ageMs < grace) {
|
|
1096
|
+
result.kept += 1;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const sourceExists = sourcePath ? existsSync3(sourcePath) : false;
|
|
1100
|
+
const agedOut = ageMs > maxAge;
|
|
1101
|
+
if (sourceExists && !agedOut) {
|
|
1102
|
+
result.kept += 1;
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
unlinkSync(markerPath);
|
|
1107
|
+
result.removed += 1;
|
|
1108
|
+
const markerId = file.slice(0, -".done".length);
|
|
1109
|
+
result.removedMarkerIds.push(markerId);
|
|
1110
|
+
log("processed marker retired", {
|
|
1111
|
+
markerId,
|
|
1112
|
+
reason: !sourceExists ? "source_missing" : "aged_out",
|
|
1113
|
+
sourcePath,
|
|
1114
|
+
ageMs
|
|
1115
|
+
});
|
|
1116
|
+
} catch {
|
|
1117
|
+
result.errors += 1;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return result;
|
|
1121
|
+
}
|
|
1122
|
+
function loadHeartbeats(commsDir) {
|
|
1123
|
+
try {
|
|
1124
|
+
return JSON.parse(readFileSync3(join4(commsDir, "heartbeats.json"), "utf8"));
|
|
1125
|
+
} catch {
|
|
1126
|
+
return {};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function shouldSkipInHeadlessMode(fileName, body) {
|
|
1130
|
+
if (process.env.TAP_HEADLESS !== "true") return false;
|
|
1131
|
+
const combined = `${fileName}
|
|
1132
|
+
${body}`;
|
|
1133
|
+
return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
|
|
1134
|
+
}
|
|
1135
|
+
function collectCandidates(inboxDir, agentId, agentName, aliasName) {
|
|
1136
|
+
return scanCandidates(inboxDir, agentId, agentName, aliasName).candidates;
|
|
721
1137
|
}
|
|
722
1138
|
function getPendingCandidates(options, cutoff) {
|
|
723
1139
|
const inboxDir = join4(options.commsDir, "inbox");
|
|
724
|
-
if (!
|
|
1140
|
+
if (!existsSync3(inboxDir)) {
|
|
725
1141
|
throw new Error(`Inbox directory not found: ${inboxDir}`);
|
|
726
1142
|
}
|
|
727
1143
|
const heartbeats = loadHeartbeats(options.commsDir);
|
|
728
1144
|
const refreshedName = refreshAgentIdentity(options, heartbeats);
|
|
729
1145
|
const cutoffMs = cutoff.getTime();
|
|
730
|
-
const
|
|
1146
|
+
const scan = scanCandidates(
|
|
731
1147
|
inboxDir,
|
|
732
1148
|
options.agentId,
|
|
733
1149
|
options.agentName,
|
|
734
1150
|
// M205: Also accept messages addressed to the heartbeat-refreshed name
|
|
735
1151
|
refreshedName !== options.agentName ? refreshedName : void 0
|
|
736
|
-
)
|
|
1152
|
+
);
|
|
1153
|
+
for (const rejection of scan.rejected) {
|
|
1154
|
+
if (rejection.mtimeMs < cutoffMs) {
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const markerPath = getProcessedMarkerPath(
|
|
1158
|
+
options.stateDir,
|
|
1159
|
+
rejection.markerId
|
|
1160
|
+
);
|
|
1161
|
+
if (existsSync3(markerPath)) {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
writeProcessedMarker(
|
|
1165
|
+
options.stateDir,
|
|
1166
|
+
{
|
|
1167
|
+
...rejection,
|
|
1168
|
+
body: ""
|
|
1169
|
+
},
|
|
1170
|
+
"rejected",
|
|
1171
|
+
null,
|
|
1172
|
+
null,
|
|
1173
|
+
rejection.rejectionReason
|
|
1174
|
+
);
|
|
1175
|
+
routingLogger.warn("envelope rejected during candidate scan", {
|
|
1176
|
+
fileName: rejection.fileName,
|
|
1177
|
+
sender: rejection.sender || "unknown",
|
|
1178
|
+
recipient: rejection.recipient || options.agentName,
|
|
1179
|
+
subject: rejection.subject || "(none)",
|
|
1180
|
+
reason: rejection.rejectionReason
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
const candidates = scan.candidates.filter((candidate) => {
|
|
737
1184
|
if (candidate.mtimeMs < cutoffMs) {
|
|
738
1185
|
return false;
|
|
739
1186
|
}
|
|
740
|
-
return !
|
|
1187
|
+
return !existsSync3(
|
|
741
1188
|
getProcessedMarkerPath(options.stateDir, candidate.markerId)
|
|
742
1189
|
);
|
|
743
1190
|
});
|
|
@@ -746,92 +1193,2209 @@ function getPendingCandidates(options, cutoff) {
|
|
|
746
1193
|
configuredName: options.agentName,
|
|
747
1194
|
refreshedName: refreshedName !== options.agentName ? refreshedName : void 0,
|
|
748
1195
|
candidateCount: candidates.length,
|
|
1196
|
+
rejectedCount: scan.rejected.length,
|
|
749
1197
|
cutoff: cutoff.toISOString()
|
|
750
1198
|
});
|
|
751
|
-
return { heartbeats, candidates };
|
|
1199
|
+
return { heartbeats, candidates };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// scripts/bridge/bridge-elicitation.ts
|
|
1203
|
+
function hasObjectShape(value) {
|
|
1204
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1205
|
+
}
|
|
1206
|
+
function isElicitationParams(value) {
|
|
1207
|
+
if (!hasObjectShape(value)) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
return "requestedSchema" in value || "mode" in value || "url" in value;
|
|
1211
|
+
}
|
|
1212
|
+
function resolveElicitationParams(raw) {
|
|
1213
|
+
if (!hasObjectShape(raw)) {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
const queue = [raw];
|
|
1217
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1218
|
+
while (queue.length > 0) {
|
|
1219
|
+
const candidate = queue.shift();
|
|
1220
|
+
if (candidate == null || visited.has(candidate)) {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
visited.add(candidate);
|
|
1224
|
+
if (isElicitationParams(candidate)) {
|
|
1225
|
+
return candidate;
|
|
1226
|
+
}
|
|
1227
|
+
if (!hasObjectShape(candidate)) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
for (const key of ["params", "request", "payload", "elicitation"]) {
|
|
1231
|
+
const nested = candidate[key];
|
|
1232
|
+
if (nested != null) {
|
|
1233
|
+
queue.push(nested);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
function firstEnumValue(values) {
|
|
1240
|
+
if (!Array.isArray(values)) {
|
|
1241
|
+
return void 0;
|
|
1242
|
+
}
|
|
1243
|
+
for (const entry of values) {
|
|
1244
|
+
if (typeof entry === "string") {
|
|
1245
|
+
return entry;
|
|
1246
|
+
}
|
|
1247
|
+
if (hasObjectShape(entry) && typeof entry.const === "string") {
|
|
1248
|
+
return entry.const;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return void 0;
|
|
1252
|
+
}
|
|
1253
|
+
function buildRequiredStringValue(schema) {
|
|
1254
|
+
return typeof schema.title === "string" && schema.title.trim() ? schema.title.trim() : "approved";
|
|
1255
|
+
}
|
|
1256
|
+
function buildElicitationFieldValue(schema, required) {
|
|
1257
|
+
const defaultValue = schema.default;
|
|
1258
|
+
if (typeof defaultValue === "string" || typeof defaultValue === "number" || typeof defaultValue === "boolean") {
|
|
1259
|
+
return defaultValue;
|
|
1260
|
+
}
|
|
1261
|
+
if (Array.isArray(defaultValue) && defaultValue.every((entry) => typeof entry === "string")) {
|
|
1262
|
+
return defaultValue;
|
|
1263
|
+
}
|
|
1264
|
+
const type = typeof schema.type === "string" ? schema.type : null;
|
|
1265
|
+
if (type === "boolean") {
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
if (type === "number" || type === "integer") {
|
|
1269
|
+
return typeof schema.minimum === "number" ? schema.minimum : 0;
|
|
1270
|
+
}
|
|
1271
|
+
if (type === "string") {
|
|
1272
|
+
return firstEnumValue(schema.enum) ?? firstEnumValue(schema.anyOf) ?? (required ? buildRequiredStringValue(schema) : "");
|
|
1273
|
+
}
|
|
1274
|
+
if (type === "array") {
|
|
1275
|
+
const minItems = typeof schema.minItems === "number" ? schema.minItems : void 0;
|
|
1276
|
+
const itemSchema = hasObjectShape(schema.items) ? schema.items : {};
|
|
1277
|
+
const itemValue = firstEnumValue(itemSchema.enum) ?? firstEnumValue(itemSchema.anyOf);
|
|
1278
|
+
if (itemValue) {
|
|
1279
|
+
return required || (minItems ?? 0) > 0 ? [itemValue] : [];
|
|
1280
|
+
}
|
|
1281
|
+
return [];
|
|
1282
|
+
}
|
|
1283
|
+
return firstEnumValue(schema.enum) ?? firstEnumValue(schema.anyOf) ?? void 0;
|
|
1284
|
+
}
|
|
1285
|
+
function isAutoElicitationRequestMethod(method) {
|
|
1286
|
+
return method === "elicitation/create" || method === "mcpServer/elicitation/request";
|
|
1287
|
+
}
|
|
1288
|
+
function buildAutoElicitationResult(rawParams) {
|
|
1289
|
+
const params = resolveElicitationParams(rawParams);
|
|
1290
|
+
if (!params) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
if (params.mode === "url" || typeof params.url === "string") {
|
|
1294
|
+
return { action: "cancel" };
|
|
1295
|
+
}
|
|
1296
|
+
const requestedSchema = hasObjectShape(params.requestedSchema) ? params.requestedSchema : null;
|
|
1297
|
+
if (!requestedSchema) {
|
|
1298
|
+
return { action: "accept" };
|
|
1299
|
+
}
|
|
1300
|
+
const properties = hasObjectShape(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
1301
|
+
const required = new Set(
|
|
1302
|
+
Array.isArray(requestedSchema.required) ? requestedSchema.required.filter(
|
|
1303
|
+
(entry) => typeof entry === "string"
|
|
1304
|
+
) : []
|
|
1305
|
+
);
|
|
1306
|
+
const content = {};
|
|
1307
|
+
for (const [field, schema] of Object.entries(properties)) {
|
|
1308
|
+
if (!hasObjectShape(schema)) {
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
const value = buildElicitationFieldValue(
|
|
1312
|
+
schema,
|
|
1313
|
+
required.has(field)
|
|
1314
|
+
);
|
|
1315
|
+
if (value !== void 0) {
|
|
1316
|
+
content[field] = value;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return Object.keys(content).length > 0 ? { action: "accept", content } : { action: "accept" };
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// scripts/bridge/bridge-dispatch.ts
|
|
1323
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
1324
|
+
import {
|
|
1325
|
+
existsSync as existsSync6,
|
|
1326
|
+
mkdirSync as mkdirSync4,
|
|
1327
|
+
readFileSync as readFileSync5,
|
|
1328
|
+
renameSync as renameSync2,
|
|
1329
|
+
statSync as statSync3,
|
|
1330
|
+
unlinkSync as unlinkSync2,
|
|
1331
|
+
writeFileSync as writeFileSync5
|
|
1332
|
+
} from "fs";
|
|
1333
|
+
import { join as join7 } from "path";
|
|
1334
|
+
|
|
1335
|
+
// src/transport/experimental/codex-ipc-control.ts
|
|
1336
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1337
|
+
|
|
1338
|
+
// src/transport/consent.ts
|
|
1339
|
+
import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
|
|
1340
|
+
import { execFileSync } from "child_process";
|
|
1341
|
+
import * as fs from "fs";
|
|
1342
|
+
import * as os from "os";
|
|
1343
|
+
import * as path from "path";
|
|
1344
|
+
var CONSENT_RECEIPTS_DIRNAME = "tap-codex-a2a-consent";
|
|
1345
|
+
var CONSENT_SECRETS_DIRNAME = "tap-codex-a2a-consent-secrets";
|
|
1346
|
+
var DEFAULT_CONSENT_TTL_SECONDS = 10 * 60;
|
|
1347
|
+
var CONSENT_METADATA_DRIFT_TOLERANCE_MS = 5e3;
|
|
1348
|
+
var CONSENT_RESERVATION_TTL_MS = 3e4;
|
|
1349
|
+
var pendingConsentReservations = /* @__PURE__ */ new Set();
|
|
1350
|
+
var SCOPE_PRIORITY = {
|
|
1351
|
+
observe: 1,
|
|
1352
|
+
suggest: 2,
|
|
1353
|
+
drive: 3
|
|
1354
|
+
};
|
|
1355
|
+
var ConsentReceiptError = class extends Error {
|
|
1356
|
+
constructor(code, message) {
|
|
1357
|
+
super(message);
|
|
1358
|
+
this.code = code;
|
|
1359
|
+
this.name = "ConsentReceiptError";
|
|
1360
|
+
}
|
|
1361
|
+
code;
|
|
1362
|
+
};
|
|
1363
|
+
function normalizeString(value) {
|
|
1364
|
+
const normalized = value?.trim();
|
|
1365
|
+
return normalized ? normalized : null;
|
|
1366
|
+
}
|
|
1367
|
+
function assertPendingReservationAvailable(consentRef) {
|
|
1368
|
+
if (!pendingConsentReservations.has(consentRef)) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
throw new ConsentReceiptError(
|
|
1372
|
+
"missing",
|
|
1373
|
+
`Consent receipt "${consentRef}" is already reserved or consumed.`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
function markPendingReservation(consentRef) {
|
|
1377
|
+
pendingConsentReservations.add(consentRef);
|
|
1378
|
+
}
|
|
1379
|
+
function clearPendingReservation(consentRef) {
|
|
1380
|
+
pendingConsentReservations.delete(consentRef);
|
|
1381
|
+
}
|
|
1382
|
+
function normalizeMethods(values) {
|
|
1383
|
+
const methods = /* @__PURE__ */ new Set();
|
|
1384
|
+
for (const value of values ?? []) {
|
|
1385
|
+
const normalized = value.trim();
|
|
1386
|
+
if (!normalized) continue;
|
|
1387
|
+
methods.add(normalized);
|
|
1388
|
+
}
|
|
1389
|
+
return [...methods].sort();
|
|
1390
|
+
}
|
|
1391
|
+
function normalizePathForComparison(value) {
|
|
1392
|
+
return path.resolve(value).replace(/\\/g, "/").toLowerCase();
|
|
1393
|
+
}
|
|
1394
|
+
function resolveReceiptsDir(explicitDir) {
|
|
1395
|
+
const configuredDir = explicitDir?.trim() || process.env.TAP_CONSENT_RECEIPTS_DIR?.trim();
|
|
1396
|
+
return configuredDir ? path.resolve(configuredDir) : path.join(os.tmpdir(), CONSENT_RECEIPTS_DIRNAME);
|
|
1397
|
+
}
|
|
1398
|
+
function resolveSecretsDir(explicitDir) {
|
|
1399
|
+
const configuredDir = explicitDir?.trim() || process.env.TAP_CONSENT_SECRETS_DIR?.trim();
|
|
1400
|
+
return configuredDir ? path.resolve(configuredDir) : path.join(os.tmpdir(), CONSENT_SECRETS_DIRNAME);
|
|
1401
|
+
}
|
|
1402
|
+
function resolveConsentDirs(options) {
|
|
1403
|
+
const receiptsDir = resolveReceiptsDir(options.receiptsDir);
|
|
1404
|
+
const secretsDir = resolveSecretsDir(options.secretsDir);
|
|
1405
|
+
if (normalizePathForComparison(receiptsDir) === normalizePathForComparison(secretsDir)) {
|
|
1406
|
+
throw new ConsentReceiptError(
|
|
1407
|
+
"invalid",
|
|
1408
|
+
"Consent receipts dir and secrets dir must be different paths."
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
return { receiptsDir, secretsDir };
|
|
1412
|
+
}
|
|
1413
|
+
function hashPairTokenBinding(options) {
|
|
1414
|
+
return createHash2("sha256").update(
|
|
1415
|
+
[
|
|
1416
|
+
options.pairToken,
|
|
1417
|
+
options.hostId ?? "",
|
|
1418
|
+
options.conversationId,
|
|
1419
|
+
options.ownerClientId ?? ""
|
|
1420
|
+
].join("\0"),
|
|
1421
|
+
"utf-8"
|
|
1422
|
+
).digest("hex");
|
|
1423
|
+
}
|
|
1424
|
+
function readUtf8PreservingTimes(filePath) {
|
|
1425
|
+
const originalStats = fs.statSync(filePath);
|
|
1426
|
+
const contents = fs.readFileSync(filePath, "utf-8");
|
|
1427
|
+
try {
|
|
1428
|
+
fs.utimesSync(filePath, originalStats.atime, originalStats.mtime);
|
|
1429
|
+
} catch {
|
|
1430
|
+
}
|
|
1431
|
+
return contents;
|
|
1432
|
+
}
|
|
1433
|
+
function loadConsentReceipt(filePath) {
|
|
1434
|
+
try {
|
|
1435
|
+
const parsed = JSON.parse(
|
|
1436
|
+
readUtf8PreservingTimes(filePath)
|
|
1437
|
+
);
|
|
1438
|
+
if (typeof parsed.id !== "string" || typeof parsed.scope !== "string" || typeof parsed.conversationId !== "string" || typeof parsed.pairTokenHash !== "string" || typeof parsed.createdAt !== "string" || typeof parsed.expiresAt !== "string") {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
if (parsed.scope !== "observe" && parsed.scope !== "suggest" && parsed.scope !== "drive") {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
return {
|
|
1445
|
+
id: parsed.id,
|
|
1446
|
+
scope: parsed.scope,
|
|
1447
|
+
hostId: normalizeString(parsed.hostId),
|
|
1448
|
+
conversationId: parsed.conversationId,
|
|
1449
|
+
ownerClientId: normalizeString(parsed.ownerClientId),
|
|
1450
|
+
issuedByClientId: normalizeString(parsed.issuedByClientId),
|
|
1451
|
+
allowedMethods: normalizeMethods(parsed.allowedMethods),
|
|
1452
|
+
pairTokenHash: parsed.pairTokenHash,
|
|
1453
|
+
createdAt: parsed.createdAt,
|
|
1454
|
+
expiresAt: parsed.expiresAt
|
|
1455
|
+
};
|
|
1456
|
+
} catch {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function loadReservedReceiptRecord(filePath) {
|
|
1461
|
+
try {
|
|
1462
|
+
const parsed = JSON.parse(readUtf8PreservingTimes(filePath));
|
|
1463
|
+
return {
|
|
1464
|
+
receipt: loadConsentReceipt(filePath),
|
|
1465
|
+
reservationOwnerId: normalizeString(parsed.reservationOwnerId)
|
|
1466
|
+
};
|
|
1467
|
+
} catch {
|
|
1468
|
+
return {
|
|
1469
|
+
receipt: null,
|
|
1470
|
+
reservationOwnerId: null
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function isExpired(receipt, now) {
|
|
1475
|
+
const expiresAtMs = new Date(receipt.expiresAt).getTime();
|
|
1476
|
+
return Number.isNaN(expiresAtMs) || expiresAtMs <= now.getTime();
|
|
1477
|
+
}
|
|
1478
|
+
function resolveSecretPath(secretsDir, receiptId) {
|
|
1479
|
+
return path.join(secretsDir, `${receiptId}.token`);
|
|
1480
|
+
}
|
|
1481
|
+
function resolveReservedReceiptPath(receiptsDir, receiptId) {
|
|
1482
|
+
return path.join(receiptsDir, `${receiptId}.reserved.json`);
|
|
1483
|
+
}
|
|
1484
|
+
function extractReceiptIdFromPath(filePath) {
|
|
1485
|
+
return path.basename(filePath).replace(/(?:\.reserved)?\.json$/i, "");
|
|
1486
|
+
}
|
|
1487
|
+
function isReceiptPath(fileName) {
|
|
1488
|
+
return /\.json$/i.test(fileName);
|
|
1489
|
+
}
|
|
1490
|
+
function resolveWindowsAclPrincipals() {
|
|
1491
|
+
const username = process.env.USERNAME?.trim();
|
|
1492
|
+
if (!username) return [];
|
|
1493
|
+
const principals = /* @__PURE__ */ new Set();
|
|
1494
|
+
const userDomain = process.env.USERDOMAIN?.trim();
|
|
1495
|
+
if (userDomain) {
|
|
1496
|
+
principals.add(`${userDomain}\\${username}`);
|
|
1497
|
+
}
|
|
1498
|
+
principals.add(username);
|
|
1499
|
+
return [...principals];
|
|
1500
|
+
}
|
|
1501
|
+
function applyWindowsPrivateAcl(targetPath) {
|
|
1502
|
+
if (process.platform !== "win32") return;
|
|
1503
|
+
const principals = resolveWindowsAclPrincipals();
|
|
1504
|
+
if (principals.length === 0) {
|
|
1505
|
+
throw new ConsentReceiptError(
|
|
1506
|
+
"invalid",
|
|
1507
|
+
`Unable to resolve a Windows principal for "${path.basename(targetPath)}".`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
let lastError = null;
|
|
1511
|
+
for (const principal of principals) {
|
|
1512
|
+
try {
|
|
1513
|
+
execFileSync(
|
|
1514
|
+
"icacls",
|
|
1515
|
+
[targetPath, "/inheritance:r", "/grant:r", `${principal}:F`],
|
|
1516
|
+
{
|
|
1517
|
+
stdio: "pipe",
|
|
1518
|
+
windowsHide: true
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1521
|
+
return;
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
lastError = error;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
throw new ConsentReceiptError(
|
|
1527
|
+
"invalid",
|
|
1528
|
+
`Failed to apply Windows ACL hardening to "${path.basename(targetPath)}": ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
function hardenSecretStorePath(targetPath, mode) {
|
|
1532
|
+
try {
|
|
1533
|
+
fs.chmodSync(targetPath, mode);
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
applyWindowsPrivateAcl(targetPath);
|
|
1537
|
+
}
|
|
1538
|
+
function hasTimestampDrift(stats, mintedAtMs) {
|
|
1539
|
+
if (!Number.isFinite(mintedAtMs)) {
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
return Math.abs(stats.mtimeMs - mintedAtMs) > CONSENT_METADATA_DRIFT_TOLERANCE_MS || Math.abs(stats.atimeMs - mintedAtMs) > CONSENT_METADATA_DRIFT_TOLERANCE_MS;
|
|
1543
|
+
}
|
|
1544
|
+
function stampMintedAt(targetPath, mintedAt) {
|
|
1545
|
+
fs.utimesSync(targetPath, mintedAt, mintedAt);
|
|
1546
|
+
}
|
|
1547
|
+
function stampReservationAt(targetPath, reservedAt) {
|
|
1548
|
+
fs.utimesSync(targetPath, reservedAt, reservedAt);
|
|
1549
|
+
}
|
|
1550
|
+
function resolveReceiptCreatedAtMs(receipt) {
|
|
1551
|
+
const createdAtMs = new Date(receipt.createdAt).getTime();
|
|
1552
|
+
if (Number.isNaN(createdAtMs)) {
|
|
1553
|
+
throw new ConsentReceiptError(
|
|
1554
|
+
"invalid",
|
|
1555
|
+
`Consent receipt "${receipt.id}" has an invalid createdAt timestamp.`
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
return createdAtMs;
|
|
1559
|
+
}
|
|
1560
|
+
function resolveReceiptCreatedAt(receipt) {
|
|
1561
|
+
return new Date(resolveReceiptCreatedAtMs(receipt));
|
|
1562
|
+
}
|
|
1563
|
+
function isReservationExpired(stats, now) {
|
|
1564
|
+
return now.getTime() - stats.mtimeMs > CONSENT_RESERVATION_TTL_MS;
|
|
1565
|
+
}
|
|
1566
|
+
function assertUntamperedConsentPath(stats, receipt, label) {
|
|
1567
|
+
if (!hasTimestampDrift(stats, resolveReceiptCreatedAtMs(receipt))) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
throw new ConsentReceiptError(
|
|
1571
|
+
"invalid",
|
|
1572
|
+
`Consent ${label} "${receipt.id}" showed timestamp drift after mint.`
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
function removeSecretPath(secretPath) {
|
|
1576
|
+
try {
|
|
1577
|
+
fs.rmSync(secretPath, { force: true });
|
|
1578
|
+
} catch {
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function removeReceiptPath(receiptPath) {
|
|
1582
|
+
try {
|
|
1583
|
+
fs.rmSync(receiptPath, { force: true });
|
|
1584
|
+
} catch {
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function writeActiveReceiptFile(filePath, receipt) {
|
|
1588
|
+
fs.writeFileSync(filePath, JSON.stringify(receipt, null, 2), "utf-8");
|
|
1589
|
+
stampMintedAt(filePath, resolveReceiptCreatedAt(receipt));
|
|
1590
|
+
}
|
|
1591
|
+
function writeReservedReceiptFile(filePath, receipt, reservationOwnerId, reservedAt) {
|
|
1592
|
+
fs.writeFileSync(
|
|
1593
|
+
filePath,
|
|
1594
|
+
JSON.stringify(
|
|
1595
|
+
{
|
|
1596
|
+
...receipt,
|
|
1597
|
+
reservationOwnerId
|
|
1598
|
+
},
|
|
1599
|
+
null,
|
|
1600
|
+
2
|
|
1601
|
+
),
|
|
1602
|
+
"utf-8"
|
|
1603
|
+
);
|
|
1604
|
+
stampReservationAt(filePath, reservedAt);
|
|
1605
|
+
}
|
|
1606
|
+
function cleanupExpiredReceipts(receiptsDir, secretsDir, now) {
|
|
1607
|
+
if (!fs.existsSync(receiptsDir)) return;
|
|
1608
|
+
for (const entry of fs.readdirSync(receiptsDir, { withFileTypes: true })) {
|
|
1609
|
+
if (!entry.isFile() || !isReceiptPath(entry.name)) continue;
|
|
1610
|
+
const filePath = path.join(receiptsDir, entry.name);
|
|
1611
|
+
const receipt = loadConsentReceipt(filePath);
|
|
1612
|
+
const receiptId = receipt?.id ?? extractReceiptIdFromPath(filePath);
|
|
1613
|
+
if (!receipt || isExpired(receipt, now)) {
|
|
1614
|
+
removeReceiptPath(filePath);
|
|
1615
|
+
removeSecretPath(resolveSecretPath(secretsDir, receiptId));
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function listReceiptPaths(receiptsDir) {
|
|
1620
|
+
if (!fs.existsSync(receiptsDir)) return [];
|
|
1621
|
+
return fs.readdirSync(receiptsDir, { withFileTypes: true }).filter(
|
|
1622
|
+
(entry) => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".reserved.json")
|
|
1623
|
+
).map((entry) => path.join(receiptsDir, entry.name)).sort();
|
|
1624
|
+
}
|
|
1625
|
+
function scopeSatisfies(actual, required) {
|
|
1626
|
+
return SCOPE_PRIORITY[actual] >= SCOPE_PRIORITY[required];
|
|
1627
|
+
}
|
|
1628
|
+
function resolveReceiptPath(receiptsDir, consentRef) {
|
|
1629
|
+
const normalizedConsentRef = normalizeString(consentRef);
|
|
1630
|
+
if (!normalizedConsentRef) return null;
|
|
1631
|
+
return path.join(receiptsDir, `${normalizedConsentRef}.json`);
|
|
1632
|
+
}
|
|
1633
|
+
function reserveReceiptPath(filePath, receipt, reservationOwnerId, now) {
|
|
1634
|
+
const reservedPath = resolveReservedReceiptPath(
|
|
1635
|
+
path.dirname(filePath),
|
|
1636
|
+
receipt.id
|
|
1637
|
+
);
|
|
1638
|
+
try {
|
|
1639
|
+
fs.renameSync(filePath, reservedPath);
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
if (error.code === "ENOENT") {
|
|
1642
|
+
throw new ConsentReceiptError(
|
|
1643
|
+
"missing",
|
|
1644
|
+
`Consent receipt "${receipt.id}" is already reserved or consumed.`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
throw error;
|
|
1648
|
+
}
|
|
1649
|
+
writeReservedReceiptFile(reservedPath, receipt, reservationOwnerId, now);
|
|
1650
|
+
return reservedPath;
|
|
1651
|
+
}
|
|
1652
|
+
function mintPairToken() {
|
|
1653
|
+
return randomBytes(32).toString("base64url");
|
|
1654
|
+
}
|
|
1655
|
+
function writeSecretFile(secretPath, pairToken, mintedAt) {
|
|
1656
|
+
fs.writeFileSync(secretPath, pairToken, {
|
|
1657
|
+
encoding: "utf-8",
|
|
1658
|
+
mode: 384
|
|
1659
|
+
});
|
|
1660
|
+
stampMintedAt(secretPath, mintedAt);
|
|
1661
|
+
hardenSecretStorePath(secretPath, 384);
|
|
1662
|
+
}
|
|
1663
|
+
function assertNoLegacyPairTokenInput(options, context) {
|
|
1664
|
+
const legacyPairToken = options.pairToken;
|
|
1665
|
+
if (typeof legacyPairToken !== "undefined") {
|
|
1666
|
+
throw new ConsentReceiptError(
|
|
1667
|
+
"invalid",
|
|
1668
|
+
`${context} no longer accepts a caller-provided pairToken.`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function createConsentReceipt(options) {
|
|
1673
|
+
assertNoLegacyPairTokenInput(options, "createConsentReceipt");
|
|
1674
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1675
|
+
const { receiptsDir, secretsDir } = resolveConsentDirs(options);
|
|
1676
|
+
const scope = options.scope ?? "drive";
|
|
1677
|
+
const conversationId = options.conversationId.trim();
|
|
1678
|
+
if (!conversationId) {
|
|
1679
|
+
throw new ConsentReceiptError(
|
|
1680
|
+
"invalid",
|
|
1681
|
+
"Consent receipt requires a non-empty conversationId."
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
fs.mkdirSync(receiptsDir, { recursive: true });
|
|
1685
|
+
fs.mkdirSync(secretsDir, { recursive: true, mode: 448 });
|
|
1686
|
+
hardenSecretStorePath(secretsDir, 448);
|
|
1687
|
+
cleanupExpiredReceipts(receiptsDir, secretsDir, now);
|
|
1688
|
+
const ttlSeconds = Math.max(
|
|
1689
|
+
1,
|
|
1690
|
+
options.ttlSeconds ?? DEFAULT_CONSENT_TTL_SECONDS
|
|
1691
|
+
);
|
|
1692
|
+
const receiptId = randomUUID();
|
|
1693
|
+
const hostId = normalizeString(options.hostId);
|
|
1694
|
+
const ownerClientId = normalizeString(options.ownerClientId);
|
|
1695
|
+
const pairToken = mintPairToken();
|
|
1696
|
+
const receipt = {
|
|
1697
|
+
id: receiptId,
|
|
1698
|
+
scope,
|
|
1699
|
+
hostId,
|
|
1700
|
+
conversationId,
|
|
1701
|
+
ownerClientId,
|
|
1702
|
+
issuedByClientId: normalizeString(options.issuedByClientId),
|
|
1703
|
+
allowedMethods: normalizeMethods(options.allowedMethods),
|
|
1704
|
+
pairTokenHash: hashPairTokenBinding({
|
|
1705
|
+
pairToken,
|
|
1706
|
+
hostId,
|
|
1707
|
+
conversationId,
|
|
1708
|
+
ownerClientId
|
|
1709
|
+
}),
|
|
1710
|
+
createdAt: now.toISOString(),
|
|
1711
|
+
expiresAt: new Date(now.getTime() + ttlSeconds * 1e3).toISOString()
|
|
1712
|
+
};
|
|
1713
|
+
const filePath = path.join(receiptsDir, `${receipt.id}.json`);
|
|
1714
|
+
const secretPath = resolveSecretPath(secretsDir, receipt.id);
|
|
1715
|
+
const createdAt = new Date(receipt.createdAt);
|
|
1716
|
+
try {
|
|
1717
|
+
writeSecretFile(secretPath, pairToken, createdAt);
|
|
1718
|
+
fs.writeFileSync(filePath, JSON.stringify(receipt, null, 2), "utf-8");
|
|
1719
|
+
stampMintedAt(filePath, createdAt);
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
removeSecretPath(secretPath);
|
|
1722
|
+
removeReceiptPath(filePath);
|
|
1723
|
+
throw error;
|
|
1724
|
+
}
|
|
1725
|
+
return { receipt, filePath };
|
|
1726
|
+
}
|
|
1727
|
+
function prepareConsentReceipt(options) {
|
|
1728
|
+
assertNoLegacyPairTokenInput(options, "consumeConsentReceipt");
|
|
1729
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1730
|
+
const { receiptsDir, secretsDir } = resolveConsentDirs(options);
|
|
1731
|
+
cleanupExpiredReceipts(receiptsDir, secretsDir, now);
|
|
1732
|
+
const requiredScope = options.requiredScope ?? "drive";
|
|
1733
|
+
const method = normalizeString(options.method);
|
|
1734
|
+
const conversationId = options.conversationId.trim();
|
|
1735
|
+
const ownerClientId = normalizeString(options.ownerClientId);
|
|
1736
|
+
const hostId = normalizeString(options.hostId);
|
|
1737
|
+
const reservationOwnerId = normalizeString(options.reservationOwnerId);
|
|
1738
|
+
const explicitConsentRef = normalizeString(options.consentRef);
|
|
1739
|
+
if (!conversationId) {
|
|
1740
|
+
throw new ConsentReceiptError(
|
|
1741
|
+
"invalid",
|
|
1742
|
+
"Consent receipt consumption requires a conversationId."
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const explicitPath = resolveReceiptPath(receiptsDir, explicitConsentRef);
|
|
1746
|
+
const explicitReservedPath = explicitConsentRef ? resolveReservedReceiptPath(receiptsDir, explicitConsentRef) : null;
|
|
1747
|
+
const reservedConsentRef = explicitConsentRef;
|
|
1748
|
+
if (reservedConsentRef && explicitPath && explicitReservedPath && !fs.existsSync(explicitPath) && fs.existsSync(explicitReservedPath)) {
|
|
1749
|
+
assertPendingReservationAvailable(reservedConsentRef);
|
|
1750
|
+
const reservedRecord = loadReservedReceiptRecord(explicitReservedPath);
|
|
1751
|
+
const reservedReceipt = reservedRecord.receipt;
|
|
1752
|
+
const reservedReceiptId = reservedReceipt?.id ?? extractReceiptIdFromPath(explicitReservedPath);
|
|
1753
|
+
if (!reservedReceipt || isExpired(reservedReceipt, now)) {
|
|
1754
|
+
removeReceiptPath(explicitReservedPath);
|
|
1755
|
+
removeSecretPath(resolveSecretPath(secretsDir, reservedReceiptId));
|
|
1756
|
+
} else if (reservationOwnerId && reservedRecord.reservationOwnerId === reservationOwnerId && isReservationExpired(fs.statSync(explicitReservedPath), now)) {
|
|
1757
|
+
fs.renameSync(explicitReservedPath, explicitPath);
|
|
1758
|
+
writeActiveReceiptFile(explicitPath, reservedReceipt);
|
|
1759
|
+
} else {
|
|
1760
|
+
throw new ConsentReceiptError(
|
|
1761
|
+
"missing",
|
|
1762
|
+
`Consent receipt "${explicitConsentRef}" is already reserved or consumed.`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
const candidatePaths = explicitPath ? [explicitPath] : listReceiptPaths(receiptsDir);
|
|
1767
|
+
let deferredError = null;
|
|
1768
|
+
for (const filePath of candidatePaths) {
|
|
1769
|
+
if (!fs.existsSync(filePath)) {
|
|
1770
|
+
if (explicitPath && explicitReservedPath && fs.existsSync(explicitReservedPath)) {
|
|
1771
|
+
throw new ConsentReceiptError(
|
|
1772
|
+
"missing",
|
|
1773
|
+
`Consent receipt "${explicitConsentRef}" is already reserved or consumed.`
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
const receiptStats = fs.statSync(filePath);
|
|
1779
|
+
const receipt = loadConsentReceipt(filePath);
|
|
1780
|
+
if (!receipt) {
|
|
1781
|
+
removeReceiptPath(filePath);
|
|
1782
|
+
removeSecretPath(
|
|
1783
|
+
resolveSecretPath(secretsDir, extractReceiptIdFromPath(filePath))
|
|
1784
|
+
);
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
if (isExpired(receipt, now)) {
|
|
1788
|
+
removeReceiptPath(filePath);
|
|
1789
|
+
removeSecretPath(resolveSecretPath(secretsDir, receipt.id));
|
|
1790
|
+
if (explicitPath) {
|
|
1791
|
+
throw new ConsentReceiptError(
|
|
1792
|
+
"expired",
|
|
1793
|
+
`Consent receipt "${receipt.id}" expired at ${receipt.expiresAt}.`
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
const secretPath = resolveSecretPath(secretsDir, receipt.id);
|
|
1799
|
+
if (!fs.existsSync(secretPath)) {
|
|
1800
|
+
if (explicitPath) {
|
|
1801
|
+
throw new ConsentReceiptError(
|
|
1802
|
+
"missing",
|
|
1803
|
+
`Consent secret "${receipt.id}" was not found.`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
let receiptPrepared = false;
|
|
1809
|
+
let cleanupSecretOnFailure = true;
|
|
1810
|
+
try {
|
|
1811
|
+
assertUntamperedConsentPath(receiptStats, receipt, "receipt");
|
|
1812
|
+
const secretStats = fs.statSync(secretPath);
|
|
1813
|
+
assertUntamperedConsentPath(secretStats, receipt, "secret");
|
|
1814
|
+
const pairToken = readUtf8PreservingTimes(secretPath).trim();
|
|
1815
|
+
if (!pairToken) {
|
|
1816
|
+
throw new ConsentReceiptError(
|
|
1817
|
+
"invalid",
|
|
1818
|
+
`Consent secret "${receipt.id}" was empty.`
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
const expectedHash = hashPairTokenBinding({
|
|
1822
|
+
pairToken,
|
|
1823
|
+
hostId,
|
|
1824
|
+
conversationId,
|
|
1825
|
+
ownerClientId
|
|
1826
|
+
});
|
|
1827
|
+
if (receipt.conversationId !== conversationId || receipt.ownerClientId !== ownerClientId || receipt.hostId !== hostId || receipt.pairTokenHash !== expectedHash) {
|
|
1828
|
+
if (explicitPath) {
|
|
1829
|
+
throw new ConsentReceiptError(
|
|
1830
|
+
"binding-mismatch",
|
|
1831
|
+
`Consent receipt "${receipt.id}" did not match the requested conversation binding.`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
if (!scopeSatisfies(receipt.scope, requiredScope)) {
|
|
1837
|
+
deferredError = new ConsentReceiptError(
|
|
1838
|
+
"scope-mismatch",
|
|
1839
|
+
`Consent receipt "${receipt.id}" grants ${receipt.scope}, not ${requiredScope}.`
|
|
1840
|
+
);
|
|
1841
|
+
if (explicitPath) throw deferredError;
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
if (method && receipt.allowedMethods.length > 0 && !receipt.allowedMethods.includes(method)) {
|
|
1845
|
+
deferredError = new ConsentReceiptError(
|
|
1846
|
+
"method-mismatch",
|
|
1847
|
+
`Consent receipt "${receipt.id}" does not allow method "${method}".`
|
|
1848
|
+
);
|
|
1849
|
+
if (explicitPath) throw deferredError;
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
let reservedReceiptPath;
|
|
1853
|
+
try {
|
|
1854
|
+
assertPendingReservationAvailable(receipt.id);
|
|
1855
|
+
reservedReceiptPath = reserveReceiptPath(
|
|
1856
|
+
filePath,
|
|
1857
|
+
receipt,
|
|
1858
|
+
reservationOwnerId,
|
|
1859
|
+
now
|
|
1860
|
+
);
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
cleanupSecretOnFailure = false;
|
|
1863
|
+
throw error;
|
|
1864
|
+
}
|
|
1865
|
+
markPendingReservation(receipt.id);
|
|
1866
|
+
receiptPrepared = true;
|
|
1867
|
+
return {
|
|
1868
|
+
receipt,
|
|
1869
|
+
commit() {
|
|
1870
|
+
if (!receiptPrepared) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
receiptPrepared = false;
|
|
1874
|
+
try {
|
|
1875
|
+
fs.rmSync(reservedReceiptPath, { force: false });
|
|
1876
|
+
} finally {
|
|
1877
|
+
clearPendingReservation(receipt.id);
|
|
1878
|
+
removeSecretPath(secretPath);
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1881
|
+
abort() {
|
|
1882
|
+
if (!receiptPrepared) {
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
receiptPrepared = false;
|
|
1886
|
+
try {
|
|
1887
|
+
fs.renameSync(reservedReceiptPath, filePath);
|
|
1888
|
+
writeActiveReceiptFile(filePath, receipt);
|
|
1889
|
+
} finally {
|
|
1890
|
+
clearPendingReservation(receipt.id);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
} finally {
|
|
1895
|
+
if (!receiptPrepared && cleanupSecretOnFailure) {
|
|
1896
|
+
removeSecretPath(secretPath);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (deferredError) {
|
|
1901
|
+
throw deferredError;
|
|
1902
|
+
}
|
|
1903
|
+
throw new ConsentReceiptError(
|
|
1904
|
+
"missing",
|
|
1905
|
+
explicitPath ? `Consent receipt "${options.consentRef}" was not found.` : "No matching consent receipt was found for the requested drive action."
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/transport/consent-ledger.ts
|
|
1910
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1911
|
+
import * as fs2 from "fs";
|
|
1912
|
+
import * as path2 from "path";
|
|
1913
|
+
function normalizeString2(value) {
|
|
1914
|
+
const normalized = value?.trim();
|
|
1915
|
+
return normalized ? normalized : null;
|
|
1916
|
+
}
|
|
1917
|
+
function normalizeAddress(value) {
|
|
1918
|
+
if (!value) {
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
const address = {
|
|
1922
|
+
hostId: normalizeString2(value.hostId),
|
|
1923
|
+
clientId: normalizeString2(value.clientId),
|
|
1924
|
+
conversationId: normalizeString2(value.conversationId),
|
|
1925
|
+
ownerClientId: normalizeString2(value.ownerClientId)
|
|
1926
|
+
};
|
|
1927
|
+
return Object.values(address).some((field) => field) ? address : null;
|
|
1928
|
+
}
|
|
1929
|
+
function isConsentLedgerEnabled() {
|
|
1930
|
+
const normalized = process.env.TAP_CONSENT_LEDGER?.trim().toLowerCase();
|
|
1931
|
+
if (!normalized) {
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
return !["0", "false", "no", "off"].includes(normalized);
|
|
1935
|
+
}
|
|
1936
|
+
function resolveConsentLedgerDir(commsDir) {
|
|
1937
|
+
const resolvedCommsDir = normalizeString2(commsDir) ?? normalizeString2(process.env.TAP_COMMS_DIR);
|
|
1938
|
+
if (!resolvedCommsDir) {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
return path2.join(
|
|
1942
|
+
path2.resolve(resolvedCommsDir),
|
|
1943
|
+
"receipts",
|
|
1944
|
+
"consent-ledger"
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
var MISSING_CONSENT_REF_ORPHAN_REASON = "missing_consent_ref";
|
|
1948
|
+
function resolveGrantId(event, grantId) {
|
|
1949
|
+
if (grantId) {
|
|
1950
|
+
return {
|
|
1951
|
+
grantId,
|
|
1952
|
+
orphanReason: null
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
if (event !== "rejected") {
|
|
1956
|
+
return {
|
|
1957
|
+
grantId: null,
|
|
1958
|
+
orphanReason: null
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
return {
|
|
1962
|
+
grantId: `orphan-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`,
|
|
1963
|
+
orphanReason: MISSING_CONSENT_REF_ORPHAN_REASON
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
function formatLedgerTimestamp(value) {
|
|
1967
|
+
return value.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
1968
|
+
}
|
|
1969
|
+
function buildLedgerFilePath(ledgerDir, record) {
|
|
1970
|
+
const timestamp = formatLedgerTimestamp(record.recordedAt);
|
|
1971
|
+
const shortGrantId = record.grantId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "unknown";
|
|
1972
|
+
const baseName = `${timestamp}-${record.event}-${shortGrantId}`;
|
|
1973
|
+
const preferredPath = path2.join(ledgerDir, `${baseName}.md`);
|
|
1974
|
+
if (!fs2.existsSync(preferredPath)) {
|
|
1975
|
+
return preferredPath;
|
|
1976
|
+
}
|
|
1977
|
+
return path2.join(
|
|
1978
|
+
ledgerDir,
|
|
1979
|
+
`${baseName}-${randomUUID2().replace(/-/g, "").slice(0, 6)}.md`
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
function buildFrontmatter(record) {
|
|
1983
|
+
const fields = [
|
|
1984
|
+
["type", "consent-ledger"],
|
|
1985
|
+
["event", record.event],
|
|
1986
|
+
["grant_id", record.grantId],
|
|
1987
|
+
["orphan_reason", record.orphanReason],
|
|
1988
|
+
["scope", record.scope],
|
|
1989
|
+
["method", record.method],
|
|
1990
|
+
["host_id", record.hostId],
|
|
1991
|
+
["conversation_id", record.conversationId],
|
|
1992
|
+
["issued_at", record.issuedAt],
|
|
1993
|
+
["expires_at", record.expiresAt],
|
|
1994
|
+
["consumed_at", record.consumedAt],
|
|
1995
|
+
["recorded_at", record.recordedAt],
|
|
1996
|
+
["result", record.result],
|
|
1997
|
+
["issued_by_client_id", record.issuedByClientId],
|
|
1998
|
+
["requester", record.requester],
|
|
1999
|
+
["owner", record.owner]
|
|
2000
|
+
];
|
|
2001
|
+
const lines = fields.map(
|
|
2002
|
+
([key, value]) => `${key}: ${JSON.stringify(value ?? null)}`
|
|
2003
|
+
);
|
|
2004
|
+
return `---
|
|
2005
|
+
${lines.join("\n")}
|
|
2006
|
+
---
|
|
2007
|
+
|
|
2008
|
+
`;
|
|
2009
|
+
}
|
|
2010
|
+
function buildBody(record) {
|
|
2011
|
+
return [
|
|
2012
|
+
"# Consent Ledger Event",
|
|
2013
|
+
"",
|
|
2014
|
+
`- Event: \`${record.event}\``,
|
|
2015
|
+
`- Grant: \`${record.grantId}\``,
|
|
2016
|
+
...record.orphanReason ? [`- Orphan Reason: \`${record.orphanReason}\``] : [],
|
|
2017
|
+
`- Scope: \`${record.scope}\``,
|
|
2018
|
+
`- Result: \`${record.result}\``,
|
|
2019
|
+
"",
|
|
2020
|
+
"## Owner",
|
|
2021
|
+
"",
|
|
2022
|
+
"```json",
|
|
2023
|
+
JSON.stringify(record.owner, null, 2),
|
|
2024
|
+
"```",
|
|
2025
|
+
"",
|
|
2026
|
+
"## Requester",
|
|
2027
|
+
"",
|
|
2028
|
+
"```json",
|
|
2029
|
+
JSON.stringify(record.requester, null, 2),
|
|
2030
|
+
"```",
|
|
2031
|
+
""
|
|
2032
|
+
].join("\n");
|
|
2033
|
+
}
|
|
2034
|
+
function writeConsentLedgerEvent(options) {
|
|
2035
|
+
if (!isConsentLedgerEnabled()) {
|
|
2036
|
+
return null;
|
|
2037
|
+
}
|
|
2038
|
+
const { grantId, orphanReason } = resolveGrantId(
|
|
2039
|
+
options.event,
|
|
2040
|
+
normalizeString2(options.grantId)
|
|
2041
|
+
);
|
|
2042
|
+
const result = normalizeString2(options.result);
|
|
2043
|
+
const ledgerDir = resolveConsentLedgerDir(options.commsDir);
|
|
2044
|
+
if (!grantId || !result || !ledgerDir) {
|
|
2045
|
+
return null;
|
|
2046
|
+
}
|
|
2047
|
+
const record = {
|
|
2048
|
+
event: options.event,
|
|
2049
|
+
grantId,
|
|
2050
|
+
orphanReason,
|
|
2051
|
+
scope: options.scope,
|
|
2052
|
+
method: normalizeString2(options.method),
|
|
2053
|
+
hostId: normalizeString2(options.hostId),
|
|
2054
|
+
conversationId: normalizeString2(options.conversationId),
|
|
2055
|
+
issuedAt: normalizeString2(options.issuedAt),
|
|
2056
|
+
expiresAt: normalizeString2(options.expiresAt),
|
|
2057
|
+
consumedAt: normalizeString2(options.consumedAt),
|
|
2058
|
+
recordedAt: normalizeString2(options.recordedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2059
|
+
result,
|
|
2060
|
+
requester: normalizeAddress(options.requester),
|
|
2061
|
+
owner: normalizeAddress(options.owner),
|
|
2062
|
+
issuedByClientId: normalizeString2(options.issuedByClientId)
|
|
2063
|
+
};
|
|
2064
|
+
try {
|
|
2065
|
+
fs2.mkdirSync(ledgerDir, { recursive: true });
|
|
2066
|
+
const filePath = buildLedgerFilePath(ledgerDir, record);
|
|
2067
|
+
fs2.writeFileSync(
|
|
2068
|
+
filePath,
|
|
2069
|
+
buildFrontmatter(record) + buildBody(record),
|
|
2070
|
+
"utf-8"
|
|
2071
|
+
);
|
|
2072
|
+
return filePath;
|
|
2073
|
+
} catch {
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// src/transport/experimental/codex-ipc-observe.ts
|
|
2079
|
+
import * as net from "net";
|
|
2080
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2081
|
+
|
|
2082
|
+
// src/transport/experimental/codex-ipc-endpoint.ts
|
|
2083
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2084
|
+
var DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH = String.raw`\\.\pipe\codex-ipc`;
|
|
2085
|
+
function normalizeDirectory(value) {
|
|
2086
|
+
return value.replace(/[\\/]+$/, "");
|
|
2087
|
+
}
|
|
2088
|
+
function resolveCodexIpcPath(options = {}) {
|
|
2089
|
+
const env = options.env ?? process.env;
|
|
2090
|
+
const explicit = env.TAP_CODEX_IPC_PATH?.trim();
|
|
2091
|
+
if (explicit) return explicit;
|
|
2092
|
+
const platform = options.platform ?? process.platform;
|
|
2093
|
+
if (platform === "win32") return DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH;
|
|
2094
|
+
if (platform === "darwin") {
|
|
2095
|
+
const baseTmp = normalizeDirectory(
|
|
2096
|
+
options.tmpDir?.trim() || env.TMPDIR?.trim() || tmpdir2()
|
|
2097
|
+
);
|
|
2098
|
+
const uid = typeof options.uid === "number" && Number.isFinite(options.uid) ? options.uid : typeof process.getuid === "function" ? process.getuid() : null;
|
|
2099
|
+
if (uid == null) {
|
|
2100
|
+
throw new Error("Cannot resolve macOS Codex IPC socket without a uid.");
|
|
2101
|
+
}
|
|
2102
|
+
return `${baseTmp}/codex-ipc/ipc-${uid}.sock`;
|
|
2103
|
+
}
|
|
2104
|
+
return DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/transport/experimental/codex-ipc-observe.ts
|
|
2108
|
+
var MAX_FRAME_BYTES = 256 * 1024 * 1024;
|
|
2109
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
|
|
2110
|
+
var DEFAULT_TARGETED_REQUEST_VERSION = 1;
|
|
2111
|
+
function isTapIpcTraceEnabled() {
|
|
2112
|
+
const value = process.env.TAP_IPC_TRACE?.trim().toLowerCase();
|
|
2113
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
2114
|
+
}
|
|
2115
|
+
function formatTraceValue(value) {
|
|
2116
|
+
if (typeof value === "string") {
|
|
2117
|
+
return JSON.stringify(value);
|
|
2118
|
+
}
|
|
2119
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
2120
|
+
return String(value);
|
|
2121
|
+
}
|
|
2122
|
+
if (value === null) {
|
|
2123
|
+
return "null";
|
|
2124
|
+
}
|
|
2125
|
+
return JSON.stringify(value);
|
|
2126
|
+
}
|
|
2127
|
+
function formatTraceContext(context) {
|
|
2128
|
+
if (!context) return "";
|
|
2129
|
+
const entries = Object.entries(context).filter(
|
|
2130
|
+
([, value]) => typeof value !== "undefined"
|
|
2131
|
+
);
|
|
2132
|
+
if (entries.length === 0) return "";
|
|
2133
|
+
return ` ${entries.map(([key, value]) => `${key}=${formatTraceValue(value)}`).join(" ")}`;
|
|
2134
|
+
}
|
|
2135
|
+
function resolveHostId(explicitHostId) {
|
|
2136
|
+
const normalizedExplicit = explicitHostId?.trim();
|
|
2137
|
+
if (normalizedExplicit) return normalizedExplicit;
|
|
2138
|
+
const computerName = process.env.COMPUTERNAME?.trim();
|
|
2139
|
+
if (computerName) return computerName;
|
|
2140
|
+
const hostName = process.env.HOSTNAME?.trim();
|
|
2141
|
+
if (hostName) return hostName;
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2144
|
+
function asRecord(value) {
|
|
2145
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2146
|
+
return value;
|
|
2147
|
+
}
|
|
2148
|
+
function asString(value) {
|
|
2149
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2150
|
+
}
|
|
2151
|
+
function getStringField(record, ...keys) {
|
|
2152
|
+
if (!record) return null;
|
|
2153
|
+
for (const key of keys) {
|
|
2154
|
+
const value = asString(record[key]);
|
|
2155
|
+
if (value) return value;
|
|
2156
|
+
}
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
function normalizeTransportAddress(hostId, clientId, conversationId, ownerClientId) {
|
|
2160
|
+
return {
|
|
2161
|
+
hostId,
|
|
2162
|
+
clientId,
|
|
2163
|
+
conversationId,
|
|
2164
|
+
ownerClientId
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
function extractConversationId(params) {
|
|
2168
|
+
return getStringField(params, "conversationId", "threadId") ?? getStringField(asRecord(params?.change), "conversationId", "threadId") ?? getStringField(asRecord(params?.thread), "id");
|
|
2169
|
+
}
|
|
2170
|
+
function listRecordKeys(value) {
|
|
2171
|
+
if (!value) return null;
|
|
2172
|
+
return Object.keys(value);
|
|
2173
|
+
}
|
|
2174
|
+
function encodeCodexIpcFrame(message) {
|
|
2175
|
+
const json = JSON.stringify(message);
|
|
2176
|
+
const payload = Buffer.from(json, "utf-8");
|
|
2177
|
+
const frame = Buffer.allocUnsafe(4 + payload.length);
|
|
2178
|
+
frame.writeUInt32LE(payload.length, 0);
|
|
2179
|
+
payload.copy(frame, 4);
|
|
2180
|
+
return frame;
|
|
2181
|
+
}
|
|
2182
|
+
function decodeCodexIpcFrames(buffer) {
|
|
2183
|
+
const messages = [];
|
|
2184
|
+
let offset = 0;
|
|
2185
|
+
while (offset + 4 <= buffer.length) {
|
|
2186
|
+
const frameLength = buffer.readUInt32LE(offset);
|
|
2187
|
+
if (frameLength > MAX_FRAME_BYTES) {
|
|
2188
|
+
throw new Error(
|
|
2189
|
+
`Codex IPC frame exceeds max size (${frameLength} bytes > ${MAX_FRAME_BYTES})`
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
if (offset + 4 + frameLength > buffer.length) break;
|
|
2193
|
+
const json = buffer.toString("utf-8", offset + 4, offset + 4 + frameLength);
|
|
2194
|
+
messages.push(JSON.parse(json));
|
|
2195
|
+
offset += 4 + frameLength;
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
messages,
|
|
2199
|
+
remainder: buffer.subarray(offset)
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
var ExperimentalCodexIpcObserveTransport = class {
|
|
2203
|
+
constructor(options = {}) {
|
|
2204
|
+
this.options = options;
|
|
2205
|
+
this.pipePath = options.pipePath ?? resolveCodexIpcPath();
|
|
2206
|
+
this.hostId = resolveHostId(options.hostId);
|
|
2207
|
+
this.clientType = options.clientType ?? "tap-observe";
|
|
2208
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2209
|
+
}
|
|
2210
|
+
options;
|
|
2211
|
+
kind = "experimental-codex-ipc-observe";
|
|
2212
|
+
pipePath;
|
|
2213
|
+
hostId;
|
|
2214
|
+
clientType;
|
|
2215
|
+
requestTimeoutMs;
|
|
2216
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2217
|
+
agents = /* @__PURE__ */ new Map();
|
|
2218
|
+
conversations = /* @__PURE__ */ new Map();
|
|
2219
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
2220
|
+
socket = null;
|
|
2221
|
+
remainder = Buffer.alloc(0);
|
|
2222
|
+
connectedAt = null;
|
|
2223
|
+
ownClientId = null;
|
|
2224
|
+
snapshot = {
|
|
2225
|
+
transport: this.kind,
|
|
2226
|
+
connected: false,
|
|
2227
|
+
connectedAt: null,
|
|
2228
|
+
agents: [],
|
|
2229
|
+
conversations: []
|
|
2230
|
+
};
|
|
2231
|
+
handleData = (...args) => {
|
|
2232
|
+
const [chunk] = args;
|
|
2233
|
+
if (!Buffer.isBuffer(chunk)) {
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
this.remainder = Buffer.concat([this.remainder, chunk]);
|
|
2237
|
+
const decoded = decodeCodexIpcFrames(this.remainder);
|
|
2238
|
+
this.remainder = decoded.remainder;
|
|
2239
|
+
for (const message of decoded.messages) {
|
|
2240
|
+
this.handleMessage(message);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
handleError = (...args) => {
|
|
2244
|
+
const [error] = args;
|
|
2245
|
+
this.rejectPendingRequests(
|
|
2246
|
+
error instanceof Error ? error : new Error(String(error ?? "Codex IPC transport error"))
|
|
2247
|
+
);
|
|
2248
|
+
};
|
|
2249
|
+
handleClose = () => {
|
|
2250
|
+
this.rejectPendingRequests(new Error("Codex IPC transport closed"));
|
|
2251
|
+
this.remainder = Buffer.alloc(0);
|
|
2252
|
+
this.emitDisconnected(null);
|
|
2253
|
+
this.detachSocket();
|
|
2254
|
+
};
|
|
2255
|
+
async connect() {
|
|
2256
|
+
if (this.socket) {
|
|
2257
|
+
await this.disconnect();
|
|
2258
|
+
}
|
|
2259
|
+
this.trace("connect:start", {
|
|
2260
|
+
pipePath: this.pipePath,
|
|
2261
|
+
clientType: this.clientType,
|
|
2262
|
+
hostId: this.hostId
|
|
2263
|
+
});
|
|
2264
|
+
const socket = this.options.socketFactory?.(this.pipePath) ?? net.createConnection({
|
|
2265
|
+
path: this.pipePath
|
|
2266
|
+
});
|
|
2267
|
+
this.socket = socket;
|
|
2268
|
+
this.attachSocket(socket);
|
|
2269
|
+
await this.waitForConnect(socket);
|
|
2270
|
+
socket.setNoDelay?.(true);
|
|
2271
|
+
this.trace("connect:open", {
|
|
2272
|
+
pipePath: this.pipePath
|
|
2273
|
+
});
|
|
2274
|
+
const response = await this.sendRequest("initialize", {
|
|
2275
|
+
clientType: this.clientType
|
|
2276
|
+
});
|
|
2277
|
+
const result = asRecord(response.result);
|
|
2278
|
+
const clientId = getStringField(result, "clientId");
|
|
2279
|
+
if (!clientId) {
|
|
2280
|
+
throw new Error("Codex IPC initialize response did not include clientId");
|
|
2281
|
+
}
|
|
2282
|
+
this.ownClientId = clientId;
|
|
2283
|
+
this.connectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2284
|
+
this.snapshot = this.buildSnapshot(true);
|
|
2285
|
+
this.trace("connect:initialized", {
|
|
2286
|
+
clientId,
|
|
2287
|
+
connectedAt: this.connectedAt,
|
|
2288
|
+
handledByClientId: response.handledByClientId ?? null,
|
|
2289
|
+
resultType: response.resultType ?? null,
|
|
2290
|
+
resultKeys: listRecordKeys(result)
|
|
2291
|
+
});
|
|
2292
|
+
this.emit({
|
|
2293
|
+
kind: "transport-connected",
|
|
2294
|
+
receivedAt: this.connectedAt,
|
|
2295
|
+
method: "initialize",
|
|
2296
|
+
sourceAddress: normalizeTransportAddress(
|
|
2297
|
+
this.hostId,
|
|
2298
|
+
this.ownClientId,
|
|
2299
|
+
null,
|
|
2300
|
+
null
|
|
2301
|
+
),
|
|
2302
|
+
payload: response,
|
|
2303
|
+
snapshot: this.snapshot
|
|
2304
|
+
});
|
|
2305
|
+
return this.snapshot;
|
|
2306
|
+
}
|
|
2307
|
+
async disconnect() {
|
|
2308
|
+
if (!this.socket) return;
|
|
2309
|
+
const socket = this.socket;
|
|
2310
|
+
this.detachSocket();
|
|
2311
|
+
this.rejectPendingRequests(new Error("Codex IPC transport disconnected"));
|
|
2312
|
+
this.remainder = Buffer.alloc(0);
|
|
2313
|
+
this.emitDisconnected({ reason: "disconnect" });
|
|
2314
|
+
socket.end();
|
|
2315
|
+
socket.destroy();
|
|
2316
|
+
}
|
|
2317
|
+
getSnapshot() {
|
|
2318
|
+
return this.snapshot;
|
|
2319
|
+
}
|
|
2320
|
+
subscribe(listener) {
|
|
2321
|
+
this.listeners.add(listener);
|
|
2322
|
+
return () => {
|
|
2323
|
+
this.listeners.delete(listener);
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
attachSocket(socket) {
|
|
2327
|
+
socket.on("data", this.handleData);
|
|
2328
|
+
socket.on("error", this.handleError);
|
|
2329
|
+
socket.on("close", this.handleClose);
|
|
2330
|
+
}
|
|
2331
|
+
emitDisconnected(payload) {
|
|
2332
|
+
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2333
|
+
this.connectedAt = null;
|
|
2334
|
+
this.snapshot = this.buildSnapshot(false);
|
|
2335
|
+
this.emit({
|
|
2336
|
+
kind: "transport-disconnected",
|
|
2337
|
+
receivedAt,
|
|
2338
|
+
method: null,
|
|
2339
|
+
sourceAddress: normalizeTransportAddress(
|
|
2340
|
+
this.hostId,
|
|
2341
|
+
this.ownClientId,
|
|
2342
|
+
null,
|
|
2343
|
+
null
|
|
2344
|
+
),
|
|
2345
|
+
payload,
|
|
2346
|
+
snapshot: this.snapshot
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
detachSocket() {
|
|
2350
|
+
if (!this.socket) return;
|
|
2351
|
+
this.socket.removeListener("data", this.handleData);
|
|
2352
|
+
this.socket.removeListener("error", this.handleError);
|
|
2353
|
+
this.socket.removeListener("close", this.handleClose);
|
|
2354
|
+
this.socket = null;
|
|
2355
|
+
}
|
|
2356
|
+
async waitForConnect(socket) {
|
|
2357
|
+
await new Promise((resolve7, reject) => {
|
|
2358
|
+
const cleanup = () => {
|
|
2359
|
+
clearTimeout(timeout);
|
|
2360
|
+
socket.removeListener("connect", onConnect);
|
|
2361
|
+
socket.removeListener("error", onError);
|
|
2362
|
+
};
|
|
2363
|
+
const onConnect = () => {
|
|
2364
|
+
cleanup();
|
|
2365
|
+
resolve7();
|
|
2366
|
+
};
|
|
2367
|
+
const onError = (...args) => {
|
|
2368
|
+
const [error] = args;
|
|
2369
|
+
cleanup();
|
|
2370
|
+
reject(
|
|
2371
|
+
error instanceof Error ? error : new Error(String(error ?? "Codex IPC connection failed"))
|
|
2372
|
+
);
|
|
2373
|
+
};
|
|
2374
|
+
const timeout = setTimeout(() => {
|
|
2375
|
+
cleanup();
|
|
2376
|
+
reject(
|
|
2377
|
+
new Error(
|
|
2378
|
+
`Timed out connecting to Codex IPC transport at ${this.pipePath}`
|
|
2379
|
+
)
|
|
2380
|
+
);
|
|
2381
|
+
}, this.requestTimeoutMs);
|
|
2382
|
+
socket.on("connect", onConnect);
|
|
2383
|
+
socket.on("error", onError);
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
getHostId() {
|
|
2387
|
+
return this.hostId;
|
|
2388
|
+
}
|
|
2389
|
+
getOwnClientId() {
|
|
2390
|
+
return this.ownClientId;
|
|
2391
|
+
}
|
|
2392
|
+
trace(message, context) {
|
|
2393
|
+
if (!isTapIpcTraceEnabled()) {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
|
|
2397
|
+
console.log(
|
|
2398
|
+
`[${timestamp}] TAP_IPC_TRACE [${this.kind}] ${message}${formatTraceContext(context)}`
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
resolveRequestVersion(_method, targetClientId) {
|
|
2402
|
+
if (this.options.protocolVersion !== null) {
|
|
2403
|
+
const configuredVersion = this.options.protocolVersion;
|
|
2404
|
+
if (typeof configuredVersion !== "undefined") {
|
|
2405
|
+
return configuredVersion;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
if (targetClientId?.trim()) {
|
|
2409
|
+
return DEFAULT_TARGETED_REQUEST_VERSION;
|
|
2410
|
+
}
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
async sendRequest(method, params, targetClientId) {
|
|
2414
|
+
if (!this.socket) {
|
|
2415
|
+
throw new Error("Codex IPC observe transport is not connected");
|
|
2416
|
+
}
|
|
2417
|
+
const requestId = randomUUID3();
|
|
2418
|
+
const message = {
|
|
2419
|
+
type: "request",
|
|
2420
|
+
requestId,
|
|
2421
|
+
method,
|
|
2422
|
+
params
|
|
2423
|
+
};
|
|
2424
|
+
if (this.ownClientId) {
|
|
2425
|
+
message.sourceClientId = this.ownClientId;
|
|
2426
|
+
}
|
|
2427
|
+
const requestVersion = this.resolveRequestVersion(method, targetClientId);
|
|
2428
|
+
if (requestVersion !== null) {
|
|
2429
|
+
message.version = requestVersion;
|
|
2430
|
+
}
|
|
2431
|
+
if (targetClientId) {
|
|
2432
|
+
message.targetClientId = targetClientId;
|
|
2433
|
+
}
|
|
2434
|
+
this.trace("request:send", {
|
|
2435
|
+
requestId,
|
|
2436
|
+
method,
|
|
2437
|
+
targetClientId: targetClientId ?? null,
|
|
2438
|
+
version: message.version ?? null,
|
|
2439
|
+
conversationId: extractConversationId(params ?? null),
|
|
2440
|
+
paramKeys: listRecordKeys(params ?? null)
|
|
2441
|
+
});
|
|
2442
|
+
const promise = new Promise((resolve7, reject) => {
|
|
2443
|
+
const timeout = setTimeout(() => {
|
|
2444
|
+
this.pendingRequests.delete(requestId);
|
|
2445
|
+
reject(
|
|
2446
|
+
new Error(
|
|
2447
|
+
`Codex IPC request "${method}" timed out after ${this.requestTimeoutMs}ms`
|
|
2448
|
+
)
|
|
2449
|
+
);
|
|
2450
|
+
}, this.requestTimeoutMs);
|
|
2451
|
+
this.pendingRequests.set(requestId, { resolve: resolve7, reject, timeout });
|
|
2452
|
+
});
|
|
2453
|
+
this.socket.write(encodeCodexIpcFrame(message));
|
|
2454
|
+
return promise;
|
|
2455
|
+
}
|
|
2456
|
+
handleMessage(message) {
|
|
2457
|
+
if (message.type === "response") {
|
|
2458
|
+
this.handleResponse(message);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
if (message.type === "broadcast") {
|
|
2462
|
+
this.handleBroadcast(message);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
handleResponse(message) {
|
|
2466
|
+
const requestId = asString(message.requestId);
|
|
2467
|
+
if (!requestId) return;
|
|
2468
|
+
const pending = this.pendingRequests.get(requestId);
|
|
2469
|
+
if (!pending) return;
|
|
2470
|
+
clearTimeout(pending.timeout);
|
|
2471
|
+
this.pendingRequests.delete(requestId);
|
|
2472
|
+
this.trace("response:recv", {
|
|
2473
|
+
requestId,
|
|
2474
|
+
method: message.method ?? null,
|
|
2475
|
+
resultType: message.resultType ?? null,
|
|
2476
|
+
handledByClientId: message.handledByClientId ?? null,
|
|
2477
|
+
hasError: message.error != null,
|
|
2478
|
+
hasResult: typeof message.result !== "undefined"
|
|
2479
|
+
});
|
|
2480
|
+
if (message.resultType === "error") {
|
|
2481
|
+
pending.reject(
|
|
2482
|
+
new Error(
|
|
2483
|
+
`Codex IPC request failed: ${JSON.stringify(message.error ?? {})}`
|
|
2484
|
+
)
|
|
2485
|
+
);
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
pending.resolve(message);
|
|
2489
|
+
}
|
|
2490
|
+
handleBroadcast(message) {
|
|
2491
|
+
const method = message.method ?? null;
|
|
2492
|
+
const params = asRecord(message.params);
|
|
2493
|
+
const sourceClientId = asString(message.sourceClientId);
|
|
2494
|
+
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2495
|
+
this.trace("broadcast:recv", {
|
|
2496
|
+
method,
|
|
2497
|
+
sourceClientId,
|
|
2498
|
+
conversationId: extractConversationId(params),
|
|
2499
|
+
version: message.version ?? null
|
|
2500
|
+
});
|
|
2501
|
+
if (method === "client-status-changed") {
|
|
2502
|
+
const clientId = getStringField(params, "clientId");
|
|
2503
|
+
if (clientId) {
|
|
2504
|
+
this.upsertAgent(clientId, {
|
|
2505
|
+
name: getStringField(params, "clientType"),
|
|
2506
|
+
metadata: {
|
|
2507
|
+
status: getStringField(params, "status"),
|
|
2508
|
+
clientType: getStringField(params, "clientType")
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
this.snapshot = this.buildSnapshot(true);
|
|
2512
|
+
this.emit({
|
|
2513
|
+
kind: "agent-status",
|
|
2514
|
+
receivedAt,
|
|
2515
|
+
method,
|
|
2516
|
+
sourceAddress: normalizeTransportAddress(
|
|
2517
|
+
this.hostId,
|
|
2518
|
+
clientId,
|
|
2519
|
+
null,
|
|
2520
|
+
null
|
|
2521
|
+
),
|
|
2522
|
+
payload: message,
|
|
2523
|
+
snapshot: this.snapshot
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
if (method === "thread-stream-state-changed") {
|
|
2529
|
+
const conversationId = extractConversationId(params);
|
|
2530
|
+
if (conversationId) {
|
|
2531
|
+
const ownerClientId = sourceClientId;
|
|
2532
|
+
if (ownerClientId) {
|
|
2533
|
+
this.upsertAgent(ownerClientId, {
|
|
2534
|
+
name: null,
|
|
2535
|
+
metadata: {}
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
this.conversations.set(conversationId, {
|
|
2539
|
+
id: conversationId,
|
|
2540
|
+
address: normalizeTransportAddress(
|
|
2541
|
+
this.hostId,
|
|
2542
|
+
ownerClientId,
|
|
2543
|
+
conversationId,
|
|
2544
|
+
ownerClientId
|
|
2545
|
+
),
|
|
2546
|
+
metadata: {
|
|
2547
|
+
change: params?.change ?? null,
|
|
2548
|
+
lastMethod: method,
|
|
2549
|
+
sourceClientId: ownerClientId
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
this.snapshot = this.buildSnapshot(true);
|
|
2553
|
+
this.emit({
|
|
2554
|
+
kind: "conversation-state",
|
|
2555
|
+
receivedAt,
|
|
2556
|
+
method,
|
|
2557
|
+
sourceAddress: normalizeTransportAddress(
|
|
2558
|
+
this.hostId,
|
|
2559
|
+
ownerClientId,
|
|
2560
|
+
conversationId,
|
|
2561
|
+
ownerClientId
|
|
2562
|
+
),
|
|
2563
|
+
payload: message,
|
|
2564
|
+
snapshot: this.snapshot
|
|
2565
|
+
});
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
this.snapshot = this.buildSnapshot(true);
|
|
2570
|
+
this.emit({
|
|
2571
|
+
kind: "raw",
|
|
2572
|
+
receivedAt,
|
|
2573
|
+
method,
|
|
2574
|
+
sourceAddress: normalizeTransportAddress(
|
|
2575
|
+
this.hostId,
|
|
2576
|
+
sourceClientId,
|
|
2577
|
+
extractConversationId(params),
|
|
2578
|
+
sourceClientId
|
|
2579
|
+
),
|
|
2580
|
+
payload: message,
|
|
2581
|
+
snapshot: this.snapshot
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
upsertAgent(clientId, update) {
|
|
2585
|
+
const existing = this.agents.get(clientId);
|
|
2586
|
+
this.agents.set(clientId, {
|
|
2587
|
+
id: clientId,
|
|
2588
|
+
name: update.name ?? existing?.name ?? null,
|
|
2589
|
+
address: normalizeTransportAddress(this.hostId, clientId, null, null),
|
|
2590
|
+
metadata: {
|
|
2591
|
+
...existing?.metadata ?? {},
|
|
2592
|
+
...update.metadata
|
|
2593
|
+
}
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
buildSnapshot(connected) {
|
|
2597
|
+
return {
|
|
2598
|
+
transport: this.kind,
|
|
2599
|
+
connected,
|
|
2600
|
+
connectedAt: connected ? this.connectedAt : null,
|
|
2601
|
+
agents: [...this.agents.values()].sort(
|
|
2602
|
+
(a, b) => a.id.localeCompare(b.id)
|
|
2603
|
+
),
|
|
2604
|
+
conversations: [...this.conversations.values()].sort(
|
|
2605
|
+
(a, b) => a.id.localeCompare(b.id)
|
|
2606
|
+
)
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
rejectPendingRequests(error) {
|
|
2610
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
2611
|
+
clearTimeout(pending.timeout);
|
|
2612
|
+
pending.reject(error);
|
|
2613
|
+
this.pendingRequests.delete(requestId);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
emit(event) {
|
|
2617
|
+
for (const listener of this.listeners) {
|
|
2618
|
+
void listener(event);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
// src/transport/experimental/codex-ipc-control.ts
|
|
2624
|
+
function asJsonRecord(value) {
|
|
2625
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
return value;
|
|
2629
|
+
}
|
|
2630
|
+
var CODEX_IPC_DRIVE_METHODS = [
|
|
2631
|
+
"thread-follower-start-turn",
|
|
2632
|
+
"thread-follower-steer-turn",
|
|
2633
|
+
"thread-follower-interrupt-turn",
|
|
2634
|
+
"thread-follower-edit-last-user-turn",
|
|
2635
|
+
"thread-follower-submit-user-input",
|
|
2636
|
+
"thread-follower-submit-mcp-server-elicitation-response",
|
|
2637
|
+
"thread-follower-command-approval-decision",
|
|
2638
|
+
"thread-follower-file-approval-decision",
|
|
2639
|
+
"thread-follower-permissions-request-approval-response",
|
|
2640
|
+
"thread-follower-compact-thread",
|
|
2641
|
+
"thread-follower-set-model-and-reasoning",
|
|
2642
|
+
"thread-follower-set-collaboration-mode",
|
|
2643
|
+
"thread-follower-set-queued-follow-ups-state"
|
|
2644
|
+
];
|
|
2645
|
+
var STABILITY_GUARDED_METHODS = /* @__PURE__ */ new Set([
|
|
2646
|
+
"thread-follower-start-turn"
|
|
2647
|
+
]);
|
|
2648
|
+
var globalLocksKey = /* @__PURE__ */ Symbol.for("tap-comms:conversationLocks");
|
|
2649
|
+
var globalDriveTimeKey = /* @__PURE__ */ Symbol.for("tap-comms:conversationLastDriveTime");
|
|
2650
|
+
var globalStabilityGuardStore = globalThis;
|
|
2651
|
+
var sharedConversationLocks = globalStabilityGuardStore[globalLocksKey] ?? /* @__PURE__ */ new Map();
|
|
2652
|
+
if (!globalStabilityGuardStore[globalLocksKey]) {
|
|
2653
|
+
globalStabilityGuardStore[globalLocksKey] = sharedConversationLocks;
|
|
2654
|
+
}
|
|
2655
|
+
var sharedConversationLastDriveTime = globalStabilityGuardStore[globalDriveTimeKey] ?? /* @__PURE__ */ new Map();
|
|
2656
|
+
if (!globalStabilityGuardStore[globalDriveTimeKey]) {
|
|
2657
|
+
globalStabilityGuardStore[globalDriveTimeKey] = sharedConversationLastDriveTime;
|
|
2658
|
+
}
|
|
2659
|
+
function normalizeAddress2(value) {
|
|
2660
|
+
return {
|
|
2661
|
+
hostId: value.hostId?.trim() || null,
|
|
2662
|
+
clientId: value.clientId?.trim() || null,
|
|
2663
|
+
conversationId: value.conversationId?.trim() || null,
|
|
2664
|
+
ownerClientId: value.ownerClientId?.trim() || null
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
function isDriveMethod(method) {
|
|
2668
|
+
return CODEX_IPC_DRIVE_METHODS.includes(method);
|
|
2669
|
+
}
|
|
2670
|
+
function normalizeMethod(method) {
|
|
2671
|
+
const normalized = method.trim();
|
|
2672
|
+
if (!isDriveMethod(normalized)) {
|
|
2673
|
+
throw new Error(`Unsupported Codex IPC drive method "${method}".`);
|
|
2674
|
+
}
|
|
2675
|
+
return normalized;
|
|
2676
|
+
}
|
|
2677
|
+
function normalizeActionLabel(action, method) {
|
|
2678
|
+
const normalized = action?.trim();
|
|
2679
|
+
return normalized || method;
|
|
2680
|
+
}
|
|
2681
|
+
function asRecord2(value) {
|
|
2682
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
return value;
|
|
2686
|
+
}
|
|
2687
|
+
function listRecordKeys2(value) {
|
|
2688
|
+
if (!value) {
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
return Object.keys(value);
|
|
2692
|
+
}
|
|
2693
|
+
function summarizeDriveParams(params) {
|
|
2694
|
+
const turnStartParams = asRecord2(params?.turnStartParams);
|
|
2695
|
+
const input = Array.isArray(turnStartParams?.input) ? turnStartParams.input : null;
|
|
2696
|
+
const textLength = input?.reduce((total, item) => {
|
|
2697
|
+
const record = asRecord2(item);
|
|
2698
|
+
return total + (typeof record?.text === "string" ? record.text.length : 0);
|
|
2699
|
+
}, 0);
|
|
2700
|
+
return {
|
|
2701
|
+
paramKeys: listRecordKeys2(params),
|
|
2702
|
+
turnStartParamKeys: listRecordKeys2(turnStartParams),
|
|
2703
|
+
inputItemCount: input?.length ?? null,
|
|
2704
|
+
textLength: textLength ?? null
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
function extractDriveTurnId(response) {
|
|
2708
|
+
const result = asRecord2(response.result);
|
|
2709
|
+
const nested = asRecord2(result?.result);
|
|
2710
|
+
const turn = asRecord2(result?.turn) ?? asRecord2(nested?.turn);
|
|
2711
|
+
const turnId = turn?.id;
|
|
2712
|
+
return typeof turnId === "string" && turnId.trim() ? turnId.trim() : null;
|
|
2713
|
+
}
|
|
2714
|
+
function extractConversationLastTurnStatus(conversation) {
|
|
2715
|
+
const change = asRecord2(conversation?.metadata.change);
|
|
2716
|
+
const turn = asRecord2(change?.turn);
|
|
2717
|
+
const turnStatus = turn?.status;
|
|
2718
|
+
if (typeof turnStatus === "string" && turnStatus.trim()) {
|
|
2719
|
+
return turnStatus.trim();
|
|
2720
|
+
}
|
|
2721
|
+
const conversationState = asRecord2(change?.conversationState);
|
|
2722
|
+
const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : null;
|
|
2723
|
+
const lastTurn = turns?.length ? asRecord2(turns[turns.length - 1]) : null;
|
|
2724
|
+
const lastStatus = lastTurn?.status;
|
|
2725
|
+
return typeof lastStatus === "string" && lastStatus.trim() ? lastStatus.trim() : null;
|
|
2726
|
+
}
|
|
2727
|
+
function extractRejectionResult(error) {
|
|
2728
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
2729
|
+
return error.code;
|
|
2730
|
+
}
|
|
2731
|
+
return "execution-rejected";
|
|
2732
|
+
}
|
|
2733
|
+
function buildFollowerStartTurnParams(options) {
|
|
2734
|
+
const turnStartParams = { ...options.turnStartParams ?? {} };
|
|
2735
|
+
const text = options.text.trim();
|
|
2736
|
+
if (!text) {
|
|
2737
|
+
throw new Error(
|
|
2738
|
+
"thread-follower-start-turn requires a non-empty text input."
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
const existingInput = Array.isArray(turnStartParams.input) ? turnStartParams.input : null;
|
|
2742
|
+
if (!existingInput) {
|
|
2743
|
+
turnStartParams.input = [
|
|
2744
|
+
{
|
|
2745
|
+
type: "text",
|
|
2746
|
+
text,
|
|
2747
|
+
text_elements: []
|
|
2748
|
+
}
|
|
2749
|
+
];
|
|
2750
|
+
}
|
|
2751
|
+
if (!Array.isArray(turnStartParams.attachments)) {
|
|
2752
|
+
turnStartParams.attachments = [];
|
|
2753
|
+
}
|
|
2754
|
+
if (!Array.isArray(turnStartParams.commentAttachments)) {
|
|
2755
|
+
turnStartParams.commentAttachments = [];
|
|
2756
|
+
}
|
|
2757
|
+
if (typeof turnStartParams.inheritThreadSettings !== "boolean") {
|
|
2758
|
+
turnStartParams.inheritThreadSettings = true;
|
|
2759
|
+
}
|
|
2760
|
+
return {
|
|
2761
|
+
conversationId: options.conversationId,
|
|
2762
|
+
turnStartParams
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
var ExperimentalCodexIpcControlTransport = class extends ExperimentalCodexIpcObserveTransport {
|
|
2766
|
+
kind = "experimental-codex-ipc-control";
|
|
2767
|
+
commsDir;
|
|
2768
|
+
receiptsDir;
|
|
2769
|
+
secretsDir;
|
|
2770
|
+
defaultConsentTtlSeconds;
|
|
2771
|
+
reservationOwnerId;
|
|
2772
|
+
conversationLocks = sharedConversationLocks;
|
|
2773
|
+
conversationLastDriveTime = sharedConversationLastDriveTime;
|
|
2774
|
+
COOLDOWN_MS = 1e4;
|
|
2775
|
+
LOCK_TIMEOUT_MS = 6e4;
|
|
2776
|
+
RECIPIENT_STATE_WAIT_MS = 750;
|
|
2777
|
+
constructor(options = {}) {
|
|
2778
|
+
super({
|
|
2779
|
+
...options,
|
|
2780
|
+
clientType: options.clientType ?? "tap-control"
|
|
2781
|
+
});
|
|
2782
|
+
this.commsDir = options.commsDir;
|
|
2783
|
+
this.receiptsDir = options.receiptsDir;
|
|
2784
|
+
this.secretsDir = options.secretsDir;
|
|
2785
|
+
this.defaultConsentTtlSeconds = options.defaultConsentTtlSeconds ?? DEFAULT_CONSENT_TTL_SECONDS;
|
|
2786
|
+
this.reservationOwnerId = options.reservationOwnerId?.trim() || randomUUID4();
|
|
2787
|
+
this.subscribe((event) => {
|
|
2788
|
+
if (event.kind === "conversation-state") {
|
|
2789
|
+
const conversationId = event.sourceAddress.conversationId;
|
|
2790
|
+
if (!conversationId) return;
|
|
2791
|
+
const payload = asJsonRecord(event.payload);
|
|
2792
|
+
const params = asJsonRecord(payload?.params);
|
|
2793
|
+
const change = asJsonRecord(params?.change);
|
|
2794
|
+
const turn = asJsonRecord(change?.turn);
|
|
2795
|
+
if (turn) {
|
|
2796
|
+
const status = turn.status;
|
|
2797
|
+
this.trace("guard:observe-turn-status", {
|
|
2798
|
+
conversationId,
|
|
2799
|
+
turnId: turn.id,
|
|
2800
|
+
status
|
|
2801
|
+
});
|
|
2802
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
2803
|
+
this.trace("guard:release-lock", {
|
|
2804
|
+
conversationId,
|
|
2805
|
+
turnId: turn.id,
|
|
2806
|
+
status
|
|
2807
|
+
});
|
|
2808
|
+
this.releaseLock(conversationId);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
acquireLock(conversationId) {
|
|
2815
|
+
const existing = this.conversationLocks.get(conversationId);
|
|
2816
|
+
if (existing?.timer) {
|
|
2817
|
+
clearTimeout(existing.timer);
|
|
2818
|
+
}
|
|
2819
|
+
const timer = setTimeout(() => {
|
|
2820
|
+
this.trace("guard:lock-timeout", { conversationId });
|
|
2821
|
+
this.conversationLocks.delete(conversationId);
|
|
2822
|
+
}, this.LOCK_TIMEOUT_MS);
|
|
2823
|
+
if (timer && typeof timer.unref === "function") {
|
|
2824
|
+
timer.unref();
|
|
2825
|
+
}
|
|
2826
|
+
this.conversationLocks.set(conversationId, { timer });
|
|
2827
|
+
}
|
|
2828
|
+
releaseLock(conversationId) {
|
|
2829
|
+
const existing = this.conversationLocks.get(conversationId);
|
|
2830
|
+
if (existing) {
|
|
2831
|
+
if (existing.timer) {
|
|
2832
|
+
clearTimeout(existing.timer);
|
|
2833
|
+
}
|
|
2834
|
+
this.conversationLocks.delete(conversationId);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
getConversationSnapshot(conversationId) {
|
|
2838
|
+
return this.getSnapshot().conversations.find(
|
|
2839
|
+
(conversation) => conversation.id === conversationId
|
|
2840
|
+
) ?? null;
|
|
2841
|
+
}
|
|
2842
|
+
async waitForConversationSnapshot(conversationId) {
|
|
2843
|
+
const existing = this.getConversationSnapshot(conversationId);
|
|
2844
|
+
if (existing) return existing;
|
|
2845
|
+
return await new Promise((resolve7) => {
|
|
2846
|
+
let unsubscribe = null;
|
|
2847
|
+
const timeout = setTimeout(() => {
|
|
2848
|
+
unsubscribe?.();
|
|
2849
|
+
resolve7(this.getConversationSnapshot(conversationId));
|
|
2850
|
+
}, this.RECIPIENT_STATE_WAIT_MS);
|
|
2851
|
+
if (typeof timeout.unref === "function") {
|
|
2852
|
+
timeout.unref();
|
|
2853
|
+
}
|
|
2854
|
+
unsubscribe = this.subscribe((event) => {
|
|
2855
|
+
if (event.kind !== "conversation-state" || event.sourceAddress.conversationId !== conversationId) {
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
clearTimeout(timeout);
|
|
2859
|
+
unsubscribe?.();
|
|
2860
|
+
resolve7(
|
|
2861
|
+
event.snapshot.conversations.find(
|
|
2862
|
+
(conversation) => conversation.id === conversationId
|
|
2863
|
+
) ?? this.getConversationSnapshot(conversationId)
|
|
2864
|
+
);
|
|
2865
|
+
});
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
async assertRecipientCanStartTurn(conversationId, method) {
|
|
2869
|
+
const conversation = await this.waitForConversationSnapshot(conversationId);
|
|
2870
|
+
const lastStatus = extractConversationLastTurnStatus(conversation);
|
|
2871
|
+
if (lastStatus === "inProgress") {
|
|
2872
|
+
this.trace("guard:recipient-active-turn", {
|
|
2873
|
+
conversationId,
|
|
2874
|
+
method,
|
|
2875
|
+
lastStatus
|
|
2876
|
+
});
|
|
2877
|
+
throw new Error(
|
|
2878
|
+
`[Stability Guard] Recipient conversation "${conversationId}" has an active in-progress turn; refusing "${method}" to avoid a stuck nested turn.`
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
createConsentReceipt(options) {
|
|
2883
|
+
const targetAddress = this.resolveConversationTargetAddress(
|
|
2884
|
+
options.conversationId,
|
|
2885
|
+
{
|
|
2886
|
+
hostId: options.hostId ?? null,
|
|
2887
|
+
ownerClientId: options.ownerClientId ?? null
|
|
2888
|
+
}
|
|
2889
|
+
);
|
|
2890
|
+
const createOptions = {
|
|
2891
|
+
receiptsDir: this.receiptsDir,
|
|
2892
|
+
secretsDir: this.secretsDir,
|
|
2893
|
+
scope: options.scope ?? "drive",
|
|
2894
|
+
hostId: targetAddress.hostId,
|
|
2895
|
+
conversationId: options.conversationId,
|
|
2896
|
+
ownerClientId: targetAddress.ownerClientId,
|
|
2897
|
+
issuedByClientId: this.getOwnClientId(),
|
|
2898
|
+
ttlSeconds: options.ttlSeconds ?? this.defaultConsentTtlSeconds,
|
|
2899
|
+
allowedMethods: [...options.allowedMethods ?? []]
|
|
2900
|
+
};
|
|
2901
|
+
const created = createConsentReceipt(createOptions);
|
|
2902
|
+
writeConsentLedgerEvent({
|
|
2903
|
+
commsDir: this.commsDir,
|
|
2904
|
+
event: "issued",
|
|
2905
|
+
grantId: created.receipt.id,
|
|
2906
|
+
scope: created.receipt.scope,
|
|
2907
|
+
method: created.receipt.allowedMethods.length === 1 ? created.receipt.allowedMethods[0] : null,
|
|
2908
|
+
hostId: created.receipt.hostId,
|
|
2909
|
+
conversationId: created.receipt.conversationId,
|
|
2910
|
+
issuedAt: created.receipt.createdAt,
|
|
2911
|
+
expiresAt: created.receipt.expiresAt,
|
|
2912
|
+
result: "granted",
|
|
2913
|
+
requester: this.buildSourceAddress(options.conversationId, targetAddress),
|
|
2914
|
+
owner: targetAddress,
|
|
2915
|
+
issuedByClientId: created.receipt.issuedByClientId
|
|
2916
|
+
});
|
|
2917
|
+
return created;
|
|
2918
|
+
}
|
|
2919
|
+
createStartTurnSuggestion(options) {
|
|
2920
|
+
return this.createSuggestion({
|
|
2921
|
+
conversationId: options.conversationId,
|
|
2922
|
+
method: "thread-follower-start-turn",
|
|
2923
|
+
params: buildFollowerStartTurnParams(options),
|
|
2924
|
+
action: options.action ?? "start-turn",
|
|
2925
|
+
consentRef: options.consentRef ?? null
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
createSuggestion(options) {
|
|
2929
|
+
const method = normalizeMethod(options.method);
|
|
2930
|
+
const targetAddress = this.resolveConversationTargetAddress(
|
|
2931
|
+
options.conversationId
|
|
2932
|
+
);
|
|
2933
|
+
const sourceAddress = this.buildSourceAddress(
|
|
2934
|
+
options.conversationId,
|
|
2935
|
+
targetAddress
|
|
2936
|
+
);
|
|
2937
|
+
return {
|
|
2938
|
+
id: randomUUID4(),
|
|
2939
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2940
|
+
status: "pending-owner-approval",
|
|
2941
|
+
scope: "suggest",
|
|
2942
|
+
method,
|
|
2943
|
+
action: normalizeActionLabel(options.action, method),
|
|
2944
|
+
conversationId: options.conversationId,
|
|
2945
|
+
payload: options.params ?? null,
|
|
2946
|
+
sourceAddress,
|
|
2947
|
+
targetAddress,
|
|
2948
|
+
consentRef: options.consentRef?.trim() || null
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
async startTurn(options) {
|
|
2952
|
+
return this.driveAction({
|
|
2953
|
+
conversationId: options.conversationId,
|
|
2954
|
+
method: "thread-follower-start-turn",
|
|
2955
|
+
params: buildFollowerStartTurnParams(options),
|
|
2956
|
+
action: options.action ?? "start-turn",
|
|
2957
|
+
consentRef: options.consentRef ?? null,
|
|
2958
|
+
hostId: options.hostId ?? null,
|
|
2959
|
+
ownerClientId: options.ownerClientId ?? null
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
async driveAction(options) {
|
|
2963
|
+
const method = normalizeMethod(options.method);
|
|
2964
|
+
const conversationId = options.conversationId.trim();
|
|
2965
|
+
const isGuarded = STABILITY_GUARDED_METHODS.has(method);
|
|
2966
|
+
const targetAddress = this.resolveConversationTargetAddress(
|
|
2967
|
+
conversationId,
|
|
2968
|
+
{
|
|
2969
|
+
hostId: options.hostId ?? null,
|
|
2970
|
+
ownerClientId: options.ownerClientId ?? null
|
|
2971
|
+
}
|
|
2972
|
+
);
|
|
2973
|
+
const ownerClientId = targetAddress.ownerClientId?.trim();
|
|
2974
|
+
if (!ownerClientId) {
|
|
2975
|
+
throw new Error(
|
|
2976
|
+
`Conversation "${conversationId}" does not have a live ownerClientId.`
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
const sourceAddress = this.buildSourceAddress(
|
|
2980
|
+
conversationId,
|
|
2981
|
+
targetAddress
|
|
2982
|
+
);
|
|
2983
|
+
this.trace("drive:prepare", {
|
|
2984
|
+
conversationId,
|
|
2985
|
+
method,
|
|
2986
|
+
action: normalizeActionLabel(options.action, method),
|
|
2987
|
+
consentRef: options.consentRef ?? null,
|
|
2988
|
+
hostId: targetAddress.hostId,
|
|
2989
|
+
ownerClientId,
|
|
2990
|
+
...summarizeDriveParams(options.params)
|
|
2991
|
+
});
|
|
2992
|
+
let preparedReceipt = null;
|
|
2993
|
+
let guardLockAcquired = false;
|
|
2994
|
+
try {
|
|
2995
|
+
preparedReceipt = prepareConsentReceipt({
|
|
2996
|
+
receiptsDir: this.receiptsDir,
|
|
2997
|
+
secretsDir: this.secretsDir,
|
|
2998
|
+
consentRef: options.consentRef ?? null,
|
|
2999
|
+
requiredScope: "drive",
|
|
3000
|
+
method,
|
|
3001
|
+
hostId: targetAddress.hostId,
|
|
3002
|
+
conversationId,
|
|
3003
|
+
ownerClientId,
|
|
3004
|
+
reservationOwnerId: this.reservationOwnerId
|
|
3005
|
+
});
|
|
3006
|
+
if (isGuarded) {
|
|
3007
|
+
await this.assertRecipientCanStartTurn(conversationId, method);
|
|
3008
|
+
if (this.conversationLocks.has(conversationId)) {
|
|
3009
|
+
this.trace("guard:locked", { conversationId, method });
|
|
3010
|
+
throw new Error(
|
|
3011
|
+
`[Stability Guard] Rejecting "${method}". Conversation "${conversationId}" has an active in-progress turn.`
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
const now = Date.now();
|
|
3015
|
+
const lastDrive = this.conversationLastDriveTime.get(conversationId) ?? 0;
|
|
3016
|
+
const elapsed = now - lastDrive;
|
|
3017
|
+
if (elapsed < this.COOLDOWN_MS) {
|
|
3018
|
+
const waitTime = this.COOLDOWN_MS - elapsed;
|
|
3019
|
+
this.trace("guard:cooldown", {
|
|
3020
|
+
conversationId,
|
|
3021
|
+
method,
|
|
3022
|
+
remainingMs: waitTime
|
|
3023
|
+
});
|
|
3024
|
+
throw new Error(
|
|
3025
|
+
`[Stability Guard] Cooldown active for "${method}" on conversation "${conversationId}". Wait ${Math.ceil(waitTime / 1e3)}s.`
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
this.acquireLock(conversationId);
|
|
3029
|
+
guardLockAcquired = true;
|
|
3030
|
+
}
|
|
3031
|
+
this.trace("drive:request", {
|
|
3032
|
+
conversationId,
|
|
3033
|
+
method,
|
|
3034
|
+
ownerClientId
|
|
3035
|
+
});
|
|
3036
|
+
const response = await this.sendRequest(
|
|
3037
|
+
method,
|
|
3038
|
+
options.params,
|
|
3039
|
+
ownerClientId
|
|
3040
|
+
);
|
|
3041
|
+
this.trace("drive:response", {
|
|
3042
|
+
conversationId,
|
|
3043
|
+
method,
|
|
3044
|
+
ownerClientId,
|
|
3045
|
+
turnId: extractDriveTurnId(response),
|
|
3046
|
+
resultType: response.resultType ?? null
|
|
3047
|
+
});
|
|
3048
|
+
preparedReceipt.commit();
|
|
3049
|
+
if (isGuarded) {
|
|
3050
|
+
this.conversationLastDriveTime.set(conversationId, Date.now());
|
|
3051
|
+
}
|
|
3052
|
+
const executedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3053
|
+
writeConsentLedgerEvent({
|
|
3054
|
+
commsDir: this.commsDir,
|
|
3055
|
+
event: "consumed",
|
|
3056
|
+
grantId: preparedReceipt.receipt.id,
|
|
3057
|
+
scope: preparedReceipt.receipt.scope,
|
|
3058
|
+
method,
|
|
3059
|
+
hostId: targetAddress.hostId,
|
|
3060
|
+
conversationId,
|
|
3061
|
+
issuedAt: preparedReceipt.receipt.createdAt,
|
|
3062
|
+
expiresAt: preparedReceipt.receipt.expiresAt,
|
|
3063
|
+
consumedAt: executedAt,
|
|
3064
|
+
recordedAt: executedAt,
|
|
3065
|
+
result: "executed",
|
|
3066
|
+
requester: sourceAddress,
|
|
3067
|
+
owner: targetAddress,
|
|
3068
|
+
issuedByClientId: preparedReceipt.receipt.issuedByClientId
|
|
3069
|
+
});
|
|
3070
|
+
return {
|
|
3071
|
+
executedAt,
|
|
3072
|
+
scope: "drive",
|
|
3073
|
+
method,
|
|
3074
|
+
action: normalizeActionLabel(options.action, method),
|
|
3075
|
+
conversationId,
|
|
3076
|
+
sourceAddress,
|
|
3077
|
+
targetAddress,
|
|
3078
|
+
consentRef: preparedReceipt.receipt.id,
|
|
3079
|
+
receipt: preparedReceipt.receipt,
|
|
3080
|
+
response
|
|
3081
|
+
};
|
|
3082
|
+
} catch (error) {
|
|
3083
|
+
if (guardLockAcquired) this.releaseLock(conversationId);
|
|
3084
|
+
this.trace("drive:error", {
|
|
3085
|
+
conversationId,
|
|
3086
|
+
method,
|
|
3087
|
+
ownerClientId,
|
|
3088
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3089
|
+
});
|
|
3090
|
+
preparedReceipt?.abort();
|
|
3091
|
+
writeConsentLedgerEvent({
|
|
3092
|
+
commsDir: this.commsDir,
|
|
3093
|
+
event: "rejected",
|
|
3094
|
+
grantId: preparedReceipt?.receipt.id ?? options.consentRef ?? null,
|
|
3095
|
+
scope: preparedReceipt?.receipt.scope ?? "drive",
|
|
3096
|
+
method,
|
|
3097
|
+
hostId: targetAddress.hostId,
|
|
3098
|
+
conversationId,
|
|
3099
|
+
issuedAt: preparedReceipt?.receipt.createdAt ?? null,
|
|
3100
|
+
expiresAt: preparedReceipt?.receipt.expiresAt ?? null,
|
|
3101
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3102
|
+
result: extractRejectionResult(error),
|
|
3103
|
+
requester: sourceAddress,
|
|
3104
|
+
owner: targetAddress,
|
|
3105
|
+
issuedByClientId: preparedReceipt?.receipt.issuedByClientId ?? this.getOwnClientId()
|
|
3106
|
+
});
|
|
3107
|
+
throw error;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
resolveConversationTargetAddress(conversationId, fallback) {
|
|
3111
|
+
const normalizedConversationId = conversationId.trim();
|
|
3112
|
+
if (!normalizedConversationId) {
|
|
3113
|
+
throw new Error(
|
|
3114
|
+
"Codex IPC control actions require a non-empty conversationId."
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
const conversation = this.getSnapshot().conversations.find(
|
|
3118
|
+
(candidate) => candidate.id === normalizedConversationId
|
|
3119
|
+
);
|
|
3120
|
+
if (conversation) {
|
|
3121
|
+
return normalizeAddress2(conversation.address);
|
|
3122
|
+
}
|
|
3123
|
+
const ownerClientId = fallback?.ownerClientId?.trim() || null;
|
|
3124
|
+
const hostId = fallback?.hostId?.trim() || this.getHostId();
|
|
3125
|
+
if (!ownerClientId) {
|
|
3126
|
+
throw new Error(
|
|
3127
|
+
`Conversation "${normalizedConversationId}" is not present in the current observe snapshot.`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
return {
|
|
3131
|
+
hostId,
|
|
3132
|
+
clientId: ownerClientId,
|
|
3133
|
+
conversationId: normalizedConversationId,
|
|
3134
|
+
ownerClientId
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
buildSourceAddress(conversationId, targetAddress) {
|
|
3138
|
+
return {
|
|
3139
|
+
hostId: this.getHostId(),
|
|
3140
|
+
clientId: this.getOwnClientId(),
|
|
3141
|
+
conversationId,
|
|
3142
|
+
ownerClientId: targetAddress.ownerClientId
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
|
|
3147
|
+
// scripts/bridge/bridge-dispatch.ts
|
|
3148
|
+
var dispatchLogger = createBridgeLogger("dispatch");
|
|
3149
|
+
var heartbeatLogger = createBridgeLogger("heartbeat");
|
|
3150
|
+
var DRIVE_DISPATCH_RESERVATION_OWNER_ID = randomUUID5();
|
|
3151
|
+
var DRIVE_NOT_YET_WIRED_REASON = "missing pairToken / drive not yet wired (M345 Phase 2 / M355 pending)";
|
|
3152
|
+
var DRIVE_ACTION_NOT_YET_SUPPORTED_REASON = "drive action is not yet wired through bridge dispatch";
|
|
3153
|
+
var DRIVE_START_TURN_ACTIONS = /* @__PURE__ */ new Set([
|
|
3154
|
+
"start-turn",
|
|
3155
|
+
"thread-follower-start-turn"
|
|
3156
|
+
]);
|
|
3157
|
+
function asRecord3(value) {
|
|
3158
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3159
|
+
return null;
|
|
3160
|
+
}
|
|
3161
|
+
return value;
|
|
3162
|
+
}
|
|
3163
|
+
function extractDriveTurnId2(result) {
|
|
3164
|
+
const response = asRecord3(result);
|
|
3165
|
+
const payload = asRecord3(response?.response);
|
|
3166
|
+
const body = asRecord3(payload?.result);
|
|
3167
|
+
const nestedResult = asRecord3(body?.result);
|
|
3168
|
+
const turn = asRecord3(body?.turn) ?? asRecord3(nestedResult?.turn);
|
|
3169
|
+
const turnId = turn?.id;
|
|
3170
|
+
return typeof turnId === "string" && turnId.trim() ? turnId.trim() : null;
|
|
3171
|
+
}
|
|
3172
|
+
function shouldTraceIpc() {
|
|
3173
|
+
const value = process.env.TAP_IPC_TRACE?.trim().toLowerCase();
|
|
3174
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
3175
|
+
}
|
|
3176
|
+
function logIpcTrace(message, context) {
|
|
3177
|
+
if (!shouldTraceIpc()) {
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
dispatchLogger.info(`[ipc-trace] ${message}`, context);
|
|
3181
|
+
}
|
|
3182
|
+
function createDriveDispatchTransport(options) {
|
|
3183
|
+
return new ExperimentalCodexIpcControlTransport({
|
|
3184
|
+
commsDir: options.commsDir,
|
|
3185
|
+
hostId: resolveBridgeHostId(options),
|
|
3186
|
+
clientType: "tap-bridge-dispatch",
|
|
3187
|
+
reservationOwnerId: DRIVE_DISPATCH_RESERVATION_OWNER_ID
|
|
3188
|
+
});
|
|
752
3189
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
756
|
-
import { join as join5 } from "path";
|
|
757
|
-
function buildUserInput(candidate, agentName, heartbeats) {
|
|
758
|
-
const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
|
|
759
|
-
const recipient = resolveAddressLabel(
|
|
760
|
-
candidate.recipient || agentName,
|
|
761
|
-
heartbeats
|
|
762
|
-
);
|
|
763
|
-
const subject = candidate.subject || "(none)";
|
|
764
|
-
const body = candidate.body.trim();
|
|
765
|
-
return [
|
|
766
|
-
`Tap-comms inbox message for ${agentName}.`,
|
|
767
|
-
`Sender: ${sender}`,
|
|
768
|
-
`Recipient: ${recipient}`,
|
|
769
|
-
`Subject: ${subject}`,
|
|
770
|
-
`File: ${candidate.fileName}`,
|
|
771
|
-
"",
|
|
772
|
-
"Message body:",
|
|
773
|
-
body || "(empty)",
|
|
774
|
-
"",
|
|
775
|
-
"---",
|
|
776
|
-
"Instructions: Read the message above and respond using the tap_reply tool.",
|
|
777
|
-
`Use tap_reply(to: "${candidate.sender || "unknown"}", subject: "<your-subject>", content: "<your-response>") to send your response.`,
|
|
778
|
-
"If the message is a review request, perform the review and reply with your findings.",
|
|
779
|
-
"If the message is informational, acknowledge briefly via tap_reply.",
|
|
780
|
-
"Do NOT respond with plain text only \u2014 you MUST use the tap_reply tool."
|
|
781
|
-
].join("\n");
|
|
3190
|
+
function buildInvalidDriveEnvelopeReason(reason) {
|
|
3191
|
+
return `invalid drive envelope: ${reason}`;
|
|
782
3192
|
}
|
|
783
|
-
function
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
3193
|
+
function normalizeDriveStartTurnAction(action) {
|
|
3194
|
+
const normalized = action?.trim() || null;
|
|
3195
|
+
if (!normalized) return null;
|
|
3196
|
+
return DRIVE_START_TURN_ACTIONS.has(normalized) ? "thread-follower-start-turn" : null;
|
|
3197
|
+
}
|
|
3198
|
+
function rejectDriveEnvelope(options, candidate, threadId, reason) {
|
|
3199
|
+
writeProcessedMarker(
|
|
3200
|
+
options.stateDir,
|
|
3201
|
+
candidate,
|
|
3202
|
+
"rejected",
|
|
791
3203
|
threadId,
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
3204
|
+
null,
|
|
3205
|
+
reason
|
|
3206
|
+
);
|
|
3207
|
+
writeLastDispatch(
|
|
3208
|
+
options.stateDir,
|
|
3209
|
+
candidate,
|
|
3210
|
+
"rejected",
|
|
3211
|
+
threadId,
|
|
3212
|
+
null,
|
|
3213
|
+
reason
|
|
800
3214
|
);
|
|
3215
|
+
writeConsentLedgerEvent({
|
|
3216
|
+
commsDir: options.commsDir,
|
|
3217
|
+
event: "rejected",
|
|
3218
|
+
grantId: candidate.consentRef?.trim() || null,
|
|
3219
|
+
scope: "drive",
|
|
3220
|
+
method: candidate.action ?? null,
|
|
3221
|
+
hostId: candidate.toAddress?.hostId ?? null,
|
|
3222
|
+
conversationId: candidate.toAddress?.conversationId ?? threadId,
|
|
3223
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3224
|
+
result: reason,
|
|
3225
|
+
requester: candidate.fromAddress ?? null,
|
|
3226
|
+
owner: candidate.toAddress ?? null
|
|
3227
|
+
});
|
|
3228
|
+
dispatchLogger.warn("rejected malformed drive envelope", {
|
|
3229
|
+
fileName: candidate.fileName,
|
|
3230
|
+
messageId: candidate.messageId ?? null,
|
|
3231
|
+
conversationId: candidate.toAddress?.conversationId ?? null,
|
|
3232
|
+
action: candidate.action ?? null,
|
|
3233
|
+
consentRef: candidate.consentRef ?? null,
|
|
3234
|
+
reason
|
|
3235
|
+
});
|
|
3236
|
+
return true;
|
|
801
3237
|
}
|
|
802
|
-
function
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
sender: candidate.sender,
|
|
808
|
-
recipient: candidate.recipient,
|
|
809
|
-
subject: candidate.subject,
|
|
810
|
-
dispatchMode,
|
|
3238
|
+
function blockDriveEnvelope(options, candidate, threadId, reason) {
|
|
3239
|
+
writeLastDispatch(
|
|
3240
|
+
options.stateDir,
|
|
3241
|
+
candidate,
|
|
3242
|
+
"blocked",
|
|
811
3243
|
threadId,
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
};
|
|
815
|
-
writeFileSync3(
|
|
816
|
-
join5(stateDir, "last-dispatch.json"),
|
|
817
|
-
`${JSON.stringify(payload, null, 2)}
|
|
818
|
-
`,
|
|
819
|
-
"utf8"
|
|
3244
|
+
null,
|
|
3245
|
+
reason
|
|
820
3246
|
);
|
|
3247
|
+
writeConsentLedgerEvent({
|
|
3248
|
+
commsDir: options.commsDir,
|
|
3249
|
+
event: "rejected",
|
|
3250
|
+
grantId: candidate.consentRef?.trim() || null,
|
|
3251
|
+
scope: "drive",
|
|
3252
|
+
method: candidate.action ?? null,
|
|
3253
|
+
hostId: candidate.toAddress?.hostId ?? null,
|
|
3254
|
+
conversationId: candidate.toAddress?.conversationId ?? threadId,
|
|
3255
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3256
|
+
result: reason,
|
|
3257
|
+
requester: candidate.fromAddress ?? null,
|
|
3258
|
+
owner: candidate.toAddress ?? null
|
|
3259
|
+
});
|
|
3260
|
+
dispatchLogger.warn("blocked drive envelope", {
|
|
3261
|
+
fileName: candidate.fileName,
|
|
3262
|
+
messageId: candidate.messageId ?? null,
|
|
3263
|
+
subject: candidate.subject || "(none)",
|
|
3264
|
+
conversationId: candidate.toAddress?.conversationId ?? null,
|
|
3265
|
+
action: candidate.action ?? null,
|
|
3266
|
+
consentRef: candidate.consentRef ?? null,
|
|
3267
|
+
reason
|
|
3268
|
+
});
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
async function dispatchDriveEnvelope(options, candidate, driveTransportFactory) {
|
|
3272
|
+
const conversationId = candidate.toAddress?.conversationId?.trim() || null;
|
|
3273
|
+
if (!conversationId) {
|
|
3274
|
+
return rejectDriveEnvelope(
|
|
3275
|
+
options,
|
|
3276
|
+
candidate,
|
|
3277
|
+
null,
|
|
3278
|
+
buildInvalidDriveEnvelopeReason(
|
|
3279
|
+
"drive scope requires target conversationId metadata."
|
|
3280
|
+
)
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3283
|
+
const consentRef = candidate.consentRef?.trim() || null;
|
|
3284
|
+
if (!consentRef) {
|
|
3285
|
+
return rejectDriveEnvelope(
|
|
3286
|
+
options,
|
|
3287
|
+
candidate,
|
|
3288
|
+
conversationId,
|
|
3289
|
+
buildInvalidDriveEnvelopeReason(
|
|
3290
|
+
"drive scope requires a non-empty consentRef."
|
|
3291
|
+
)
|
|
3292
|
+
);
|
|
3293
|
+
}
|
|
3294
|
+
const method = normalizeDriveStartTurnAction(candidate.action);
|
|
3295
|
+
if (!method) {
|
|
3296
|
+
const action = candidate.action?.trim() || "(missing)";
|
|
3297
|
+
return blockDriveEnvelope(
|
|
3298
|
+
options,
|
|
3299
|
+
candidate,
|
|
3300
|
+
conversationId,
|
|
3301
|
+
`${DRIVE_ACTION_NOT_YET_SUPPORTED_REASON}: ${action}`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
const text = candidate.body.trim();
|
|
3305
|
+
if (!text) {
|
|
3306
|
+
return rejectDriveEnvelope(
|
|
3307
|
+
options,
|
|
3308
|
+
candidate,
|
|
3309
|
+
conversationId,
|
|
3310
|
+
buildInvalidDriveEnvelopeReason(
|
|
3311
|
+
`${method} requires a non-empty message body.`
|
|
3312
|
+
)
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
const transport = driveTransportFactory(options);
|
|
3316
|
+
const targetHostId = candidate.toAddress?.hostId?.trim() || null;
|
|
3317
|
+
const targetOwnerClientId = candidate.toAddress?.ownerClientId?.trim() || candidate.toAddress?.clientId?.trim() || null;
|
|
3318
|
+
logIpcTrace("drive envelope prepared", {
|
|
3319
|
+
fileName: candidate.fileName,
|
|
3320
|
+
conversationId,
|
|
3321
|
+
action: candidate.action ?? null,
|
|
3322
|
+
consentRef,
|
|
3323
|
+
targetHostId,
|
|
3324
|
+
targetOwnerClientId
|
|
3325
|
+
});
|
|
3326
|
+
try {
|
|
3327
|
+
logIpcTrace("transport connect start", {
|
|
3328
|
+
fileName: candidate.fileName,
|
|
3329
|
+
conversationId
|
|
3330
|
+
});
|
|
3331
|
+
await transport.connect();
|
|
3332
|
+
logIpcTrace("transport connect success", {
|
|
3333
|
+
fileName: candidate.fileName,
|
|
3334
|
+
conversationId
|
|
3335
|
+
});
|
|
3336
|
+
logIpcTrace("transport startTurn start", {
|
|
3337
|
+
fileName: candidate.fileName,
|
|
3338
|
+
conversationId,
|
|
3339
|
+
textLength: text.length
|
|
3340
|
+
});
|
|
3341
|
+
const result = await transport.startTurn({
|
|
3342
|
+
conversationId,
|
|
3343
|
+
text,
|
|
3344
|
+
action: candidate.action?.trim() || null,
|
|
3345
|
+
consentRef,
|
|
3346
|
+
hostId: targetHostId,
|
|
3347
|
+
ownerClientId: targetOwnerClientId
|
|
3348
|
+
});
|
|
3349
|
+
const turnId = extractDriveTurnId2(result);
|
|
3350
|
+
logIpcTrace("transport startTurn success", {
|
|
3351
|
+
fileName: candidate.fileName,
|
|
3352
|
+
conversationId,
|
|
3353
|
+
turnId,
|
|
3354
|
+
result
|
|
3355
|
+
});
|
|
3356
|
+
writeProcessedMarker(
|
|
3357
|
+
options.stateDir,
|
|
3358
|
+
candidate,
|
|
3359
|
+
"drive",
|
|
3360
|
+
conversationId,
|
|
3361
|
+
turnId
|
|
3362
|
+
);
|
|
3363
|
+
writeLastDispatch(
|
|
3364
|
+
options.stateDir,
|
|
3365
|
+
candidate,
|
|
3366
|
+
"drive",
|
|
3367
|
+
conversationId,
|
|
3368
|
+
turnId,
|
|
3369
|
+
null
|
|
3370
|
+
);
|
|
3371
|
+
markBridgeActivity();
|
|
3372
|
+
dispatchLogger.info("handed drive envelope to control transport", {
|
|
3373
|
+
fileName: candidate.fileName,
|
|
3374
|
+
messageId: candidate.messageId ?? null,
|
|
3375
|
+
conversationId,
|
|
3376
|
+
action: candidate.action ?? null,
|
|
3377
|
+
consentRef,
|
|
3378
|
+
turnId
|
|
3379
|
+
});
|
|
3380
|
+
return true;
|
|
3381
|
+
} catch (error) {
|
|
3382
|
+
logIpcTrace("transport startTurn error", {
|
|
3383
|
+
fileName: candidate.fileName,
|
|
3384
|
+
conversationId,
|
|
3385
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3386
|
+
});
|
|
3387
|
+
return blockDriveEnvelope(
|
|
3388
|
+
options,
|
|
3389
|
+
candidate,
|
|
3390
|
+
conversationId,
|
|
3391
|
+
sanitizeErrorForPersistence(
|
|
3392
|
+
error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3393
|
+
) ?? "drive handoff failed"
|
|
3394
|
+
);
|
|
3395
|
+
} finally {
|
|
3396
|
+
await transport.disconnect().catch(() => void 0);
|
|
3397
|
+
}
|
|
821
3398
|
}
|
|
822
|
-
|
|
823
|
-
// scripts/bridge/bridge-dispatch.ts
|
|
824
|
-
import {
|
|
825
|
-
existsSync as existsSync5,
|
|
826
|
-
readFileSync as readFileSync5,
|
|
827
|
-
renameSync as renameSync2,
|
|
828
|
-
statSync as statSync2,
|
|
829
|
-
unlinkSync,
|
|
830
|
-
writeFileSync as writeFileSync4
|
|
831
|
-
} from "fs";
|
|
832
|
-
import { join as join6 } from "path";
|
|
833
|
-
var dispatchLogger = createBridgeLogger("dispatch");
|
|
834
|
-
var heartbeatLogger = createBridgeLogger("heartbeat");
|
|
835
3399
|
function sanitizeErrorForPersistence(error) {
|
|
836
3400
|
if (!error) return null;
|
|
837
3401
|
return error.replace(/([?&])tap_token=[^\s&)"'}]+/gi, "$1tap_token=***").replace(/([?&])token=[^\s&)"'}]+/gi, "$1token=***").replace(/([?&])secret=[^\s&)"'}]+/gi, "$1secret=***").replace(/([?&])key=[^\s&)"'}]+/gi, "$1key=***").replace(/"tap_token"\s*:\s*"[^"]*"/g, '"tap_token":"***"').replace(/"token"\s*:\s*"[^"]*"/g, '"token":"***"').replace(/"secret"\s*:\s*"[^"]*"/g, '"secret":"***"').replace(/"password"\s*:\s*"[^"]*"/g, '"password":"***"').replace(/"authorization"\s*:\s*"[^"]*"/gi, '"authorization":"***"').replace(/tap-auth-[A-Za-z0-9_.\-/+=]+/g, "tap-auth-***").replace(/Bearer\s+[A-Za-z0-9_.\-/+=]+/gi, "Bearer ***").replace(/(?<=[=:"\s])[A-Za-z0-9_\-/+=]{40,}(?=["\s&)}'}\],]|$)/g, "***");
|
|
@@ -841,9 +3405,58 @@ function delay(ms) {
|
|
|
841
3405
|
setTimeout(resolvePromise, ms);
|
|
842
3406
|
});
|
|
843
3407
|
}
|
|
3408
|
+
function resolveBridgeRoutingSlot(agentId) {
|
|
3409
|
+
const normalized = agentId.trim().replace(/-/g, "_").toLowerCase();
|
|
3410
|
+
if (!normalized) return null;
|
|
3411
|
+
if (normalized === "tower" || normalized === "claude_main" || normalized === "codex_main") {
|
|
3412
|
+
return "tower";
|
|
3413
|
+
}
|
|
3414
|
+
if (normalized === "reviewer" || normalized === "claude_reviewer" || normalized === "codex_reviewer") {
|
|
3415
|
+
return "reviewer";
|
|
3416
|
+
}
|
|
3417
|
+
const worktreeMatch = normalized.match(/^(?:(?:claude|codex)_)?wt_?(\d+)$/);
|
|
3418
|
+
if (!worktreeMatch) return null;
|
|
3419
|
+
return `wt-${Number.parseInt(worktreeMatch[1], 10)}`;
|
|
3420
|
+
}
|
|
3421
|
+
function resolveBridgeHostId(options) {
|
|
3422
|
+
const explicitHostId = process.env.TAP_HOST_ID?.trim();
|
|
3423
|
+
if (explicitHostId) return explicitHostId;
|
|
3424
|
+
const computerName = process.env.COMPUTERNAME?.trim();
|
|
3425
|
+
if (computerName) return computerName;
|
|
3426
|
+
const hostName = process.env.HOSTNAME?.trim();
|
|
3427
|
+
if (hostName) return hostName;
|
|
3428
|
+
return options.commsDir;
|
|
3429
|
+
}
|
|
3430
|
+
function resolveBridgeAliases(values) {
|
|
3431
|
+
const aliases = [];
|
|
3432
|
+
for (const value of values) {
|
|
3433
|
+
const normalized = value?.trim();
|
|
3434
|
+
if (!normalized || aliases.includes(normalized)) continue;
|
|
3435
|
+
aliases.push(normalized);
|
|
3436
|
+
}
|
|
3437
|
+
return aliases;
|
|
3438
|
+
}
|
|
3439
|
+
function buildBridgeAddress(options, conversationId) {
|
|
3440
|
+
const slot = options.routingSlot ?? resolveBridgeRoutingSlot(options.agentId);
|
|
3441
|
+
const routingAddress = slot ?? options.agentId;
|
|
3442
|
+
return {
|
|
3443
|
+
hostId: resolveBridgeHostId(options),
|
|
3444
|
+
clientId: options.agentId,
|
|
3445
|
+
conversationId,
|
|
3446
|
+
ownerClientId: conversationId ? options.agentId : null,
|
|
3447
|
+
routingAddress,
|
|
3448
|
+
slot,
|
|
3449
|
+
aliases: resolveBridgeAliases([
|
|
3450
|
+
routingAddress,
|
|
3451
|
+
slot,
|
|
3452
|
+
options.agentId,
|
|
3453
|
+
options.agentName
|
|
3454
|
+
])
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
844
3457
|
function readThreadState(stateDir) {
|
|
845
|
-
const threadPath =
|
|
846
|
-
if (!
|
|
3458
|
+
const threadPath = join7(stateDir, "thread.json");
|
|
3459
|
+
if (!existsSync6(threadPath)) {
|
|
847
3460
|
return null;
|
|
848
3461
|
}
|
|
849
3462
|
try {
|
|
@@ -851,7 +3464,10 @@ function readThreadState(stateDir) {
|
|
|
851
3464
|
readFileSync5(threadPath, "utf8")
|
|
852
3465
|
);
|
|
853
3466
|
if (parsed.threadId) {
|
|
854
|
-
return
|
|
3467
|
+
return {
|
|
3468
|
+
...parsed,
|
|
3469
|
+
cwd: normalizePersistedThreadCwd(parsed.cwd)
|
|
3470
|
+
};
|
|
855
3471
|
}
|
|
856
3472
|
} catch {
|
|
857
3473
|
return null;
|
|
@@ -864,10 +3480,10 @@ function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
|
|
|
864
3480
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
865
3481
|
appServerUrl,
|
|
866
3482
|
ephemeral,
|
|
867
|
-
cwd
|
|
3483
|
+
cwd: normalizePersistedThreadCwd(cwd)
|
|
868
3484
|
};
|
|
869
|
-
|
|
870
|
-
|
|
3485
|
+
writeFileSync5(
|
|
3486
|
+
join7(stateDir, "thread.json"),
|
|
871
3487
|
`${JSON.stringify(payload, null, 2)}
|
|
872
3488
|
`,
|
|
873
3489
|
"utf8"
|
|
@@ -877,15 +3493,15 @@ function acquireCommsLock(lockPath) {
|
|
|
877
3493
|
const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
|
|
878
3494
|
while (Date.now() < deadline) {
|
|
879
3495
|
try {
|
|
880
|
-
|
|
3496
|
+
writeFileSync5(lockPath, String(process.pid), { flag: "wx" });
|
|
881
3497
|
return true;
|
|
882
3498
|
} catch {
|
|
883
3499
|
try {
|
|
884
|
-
const lockAge = Date.now() -
|
|
3500
|
+
const lockAge = Date.now() - statSync3(lockPath).mtimeMs;
|
|
885
3501
|
if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
|
|
886
|
-
|
|
3502
|
+
unlinkSync2(lockPath);
|
|
887
3503
|
try {
|
|
888
|
-
|
|
3504
|
+
writeFileSync5(lockPath, String(process.pid), { flag: "wx" });
|
|
889
3505
|
return true;
|
|
890
3506
|
} catch {
|
|
891
3507
|
}
|
|
@@ -901,49 +3517,94 @@ function acquireCommsLock(lockPath) {
|
|
|
901
3517
|
}
|
|
902
3518
|
function releaseCommsLock(lockPath) {
|
|
903
3519
|
try {
|
|
904
|
-
|
|
3520
|
+
unlinkSync2(lockPath);
|
|
905
3521
|
} catch {
|
|
906
3522
|
}
|
|
907
3523
|
}
|
|
908
|
-
function
|
|
909
|
-
const
|
|
910
|
-
|
|
3524
|
+
function heartbeatStoreKey(record) {
|
|
3525
|
+
for (const field of ["id", "instanceId", "agent"]) {
|
|
3526
|
+
const value = record[field];
|
|
3527
|
+
if (typeof value === "string" && value.trim()) {
|
|
3528
|
+
return value.trim();
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
return null;
|
|
3532
|
+
}
|
|
3533
|
+
function normalizeHeartbeatStore(raw) {
|
|
3534
|
+
if (Array.isArray(raw)) {
|
|
3535
|
+
const normalized = {};
|
|
3536
|
+
for (const entry of raw) {
|
|
3537
|
+
const record = asRecord3(entry);
|
|
3538
|
+
if (!record) continue;
|
|
3539
|
+
const key = heartbeatStoreKey(record);
|
|
3540
|
+
if (key) normalized[key] = record;
|
|
3541
|
+
}
|
|
3542
|
+
return normalized;
|
|
3543
|
+
}
|
|
3544
|
+
return asRecord3(raw) ?? {};
|
|
3545
|
+
}
|
|
3546
|
+
function updateCommsHeartbeat(options, status, conversationId) {
|
|
3547
|
+
const heartbeatsPath = join7(options.commsDir, "heartbeats.json");
|
|
3548
|
+
const lockPath = join7(options.commsDir, ".heartbeats.lock");
|
|
911
3549
|
if (!acquireCommsLock(lockPath)) {
|
|
912
3550
|
return;
|
|
913
3551
|
}
|
|
914
3552
|
try {
|
|
915
3553
|
let store = {};
|
|
916
3554
|
try {
|
|
917
|
-
store =
|
|
3555
|
+
store = normalizeHeartbeatStore(
|
|
3556
|
+
JSON.parse(readFileSync5(heartbeatsPath, "utf-8"))
|
|
3557
|
+
);
|
|
918
3558
|
} catch {
|
|
919
3559
|
}
|
|
920
3560
|
const key = options.agentId;
|
|
921
3561
|
const existing = store[key];
|
|
922
3562
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3563
|
+
const lastActivity = _lastBridgeActivityAt ?? existing?.lastActivity ?? now;
|
|
3564
|
+
const resolvedConversationId = conversationId ?? readThreadState(options.stateDir)?.threadId ?? null;
|
|
923
3565
|
store[key] = {
|
|
924
3566
|
id: options.agentId,
|
|
925
3567
|
agent: options.agentName,
|
|
926
3568
|
timestamp: now,
|
|
927
|
-
lastActivity
|
|
3569
|
+
lastActivity,
|
|
928
3570
|
joinedAt: existing?.joinedAt ?? now,
|
|
929
3571
|
status,
|
|
930
3572
|
source: "bridge-dispatch",
|
|
931
3573
|
instanceId: options.agentId,
|
|
932
3574
|
bridgePid: process.pid,
|
|
933
|
-
connectHash: `instance:${options.agentId}
|
|
3575
|
+
connectHash: `instance:${options.agentId}`,
|
|
3576
|
+
receiveTransports: ["consent-drive"],
|
|
3577
|
+
address: buildBridgeAddress(options, resolvedConversationId)
|
|
934
3578
|
};
|
|
935
3579
|
const tmpPath = heartbeatsPath + ".tmp." + process.pid;
|
|
936
|
-
|
|
3580
|
+
writeFileSync5(tmpPath, JSON.stringify(store, null, 2), "utf-8");
|
|
937
3581
|
renameSync2(tmpPath, heartbeatsPath);
|
|
3582
|
+
try {
|
|
3583
|
+
const presenceDir = join7(options.commsDir, "presence");
|
|
3584
|
+
mkdirSync4(presenceDir, { recursive: true });
|
|
3585
|
+
const sanitizedId = key.replace(/[/\\:]/g, "_");
|
|
3586
|
+
const presPath = join7(presenceDir, `${sanitizedId}.json`);
|
|
3587
|
+
const presTmp = presPath + ".tmp." + process.pid;
|
|
3588
|
+
writeFileSync5(presTmp, JSON.stringify(store[key], null, 2), "utf-8");
|
|
3589
|
+
renameSync2(presTmp, presPath);
|
|
3590
|
+
} catch {
|
|
3591
|
+
}
|
|
938
3592
|
} catch {
|
|
939
3593
|
} finally {
|
|
940
3594
|
releaseCommsLock(lockPath);
|
|
941
3595
|
}
|
|
942
3596
|
}
|
|
943
3597
|
var heartbeatCount = 0;
|
|
3598
|
+
var _lastBridgeActivityAt = null;
|
|
3599
|
+
function markBridgeActivity() {
|
|
3600
|
+
_lastBridgeActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3601
|
+
}
|
|
3602
|
+
function getLastBridgeActivityAt() {
|
|
3603
|
+
return _lastBridgeActivityAt;
|
|
3604
|
+
}
|
|
944
3605
|
function readPreviousHeartbeat(stateDir) {
|
|
945
|
-
const heartbeatPath =
|
|
946
|
-
if (!
|
|
3606
|
+
const heartbeatPath = join7(stateDir, "heartbeat.json");
|
|
3607
|
+
if (!existsSync6(heartbeatPath)) {
|
|
947
3608
|
return null;
|
|
948
3609
|
}
|
|
949
3610
|
try {
|
|
@@ -955,8 +3616,8 @@ function readPreviousHeartbeat(stateDir) {
|
|
|
955
3616
|
}
|
|
956
3617
|
}
|
|
957
3618
|
function readLastDispatchAt(stateDir) {
|
|
958
|
-
const dispatchPath =
|
|
959
|
-
if (!
|
|
3619
|
+
const dispatchPath = join7(stateDir, "last-dispatch.json");
|
|
3620
|
+
if (!existsSync6(dispatchPath)) {
|
|
960
3621
|
return null;
|
|
961
3622
|
}
|
|
962
3623
|
try {
|
|
@@ -968,10 +3629,6 @@ function readLastDispatchAt(stateDir) {
|
|
|
968
3629
|
return null;
|
|
969
3630
|
}
|
|
970
3631
|
}
|
|
971
|
-
function isWaitingApprovalStatus(status) {
|
|
972
|
-
if (!status) return false;
|
|
973
|
-
return /approval|input-required|confirm|consent/i.test(status);
|
|
974
|
-
}
|
|
975
3632
|
function resolveTurnState(client) {
|
|
976
3633
|
if (!client) return null;
|
|
977
3634
|
if (client.activeTurnId) return "active";
|
|
@@ -987,7 +3644,11 @@ function writeHeartbeat(options, client, health) {
|
|
|
987
3644
|
const previousHeartbeat = readPreviousHeartbeat(options.stateDir);
|
|
988
3645
|
const lastDispatchAt = readLastDispatchAt(options.stateDir);
|
|
989
3646
|
const turnState = resolveTurnState(client);
|
|
990
|
-
const
|
|
3647
|
+
const turnJustCompleted = previousHeartbeat?.activeTurnId && !client?.activeTurnId;
|
|
3648
|
+
if (turnJustCompleted) {
|
|
3649
|
+
markBridgeActivity();
|
|
3650
|
+
}
|
|
3651
|
+
const lastTurnAt = turnJustCompleted ? nowIso : previousHeartbeat?.lastTurnAt ?? null;
|
|
991
3652
|
const idleSince = turnState === "idle" || turnState === "waiting-approval" ? previousHeartbeat?.turnState === turnState && previousHeartbeat.idleSince ? previousHeartbeat.idleSince : lastTurnAt ?? lastDispatchAt ?? nowIso : null;
|
|
992
3653
|
if (client?.threadId) {
|
|
993
3654
|
const savedThread = readThreadState(options.stateDir);
|
|
@@ -1025,8 +3686,8 @@ function writeHeartbeat(options, client, health) {
|
|
|
1025
3686
|
consecutiveFailureCount: health.consecutiveFailureCount,
|
|
1026
3687
|
busyMode: options.busyMode
|
|
1027
3688
|
};
|
|
1028
|
-
|
|
1029
|
-
|
|
3689
|
+
writeFileSync5(
|
|
3690
|
+
join7(options.stateDir, "heartbeat.json"),
|
|
1030
3691
|
`${JSON.stringify(payload, null, 2)}
|
|
1031
3692
|
`,
|
|
1032
3693
|
"utf8"
|
|
@@ -1041,19 +3702,38 @@ function writeHeartbeat(options, client, health) {
|
|
|
1041
3702
|
});
|
|
1042
3703
|
}
|
|
1043
3704
|
const status = turnState === "active" ? "active" : "idle";
|
|
1044
|
-
updateCommsHeartbeat(
|
|
3705
|
+
updateCommsHeartbeat(
|
|
3706
|
+
options,
|
|
3707
|
+
status,
|
|
3708
|
+
payload.threadId ?? readThreadState(options.stateDir)?.threadId ?? null
|
|
3709
|
+
);
|
|
1045
3710
|
}
|
|
1046
|
-
async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
1047
|
-
const input = buildUserInput(candidate, options.agentName, heartbeats);
|
|
3711
|
+
async function dispatchCandidate(client, options, candidate, heartbeats, driveTransportFactory = createDriveDispatchTransport) {
|
|
1048
3712
|
dispatchLogger.info("dispatching candidate", {
|
|
1049
3713
|
sender: candidate.sender || "unknown",
|
|
1050
3714
|
recipient: candidate.recipient || options.agentName,
|
|
1051
3715
|
subject: candidate.subject || "(none)",
|
|
1052
3716
|
fileName: candidate.fileName,
|
|
3717
|
+
messageId: candidate.messageId ?? null,
|
|
3718
|
+
scope: candidate.scope ?? null,
|
|
3719
|
+
action: candidate.action ?? null,
|
|
3720
|
+
hasConsentRef: Boolean(candidate.consentRef),
|
|
1053
3721
|
threadId: client.threadId,
|
|
1054
3722
|
activeTurnId: client.activeTurnId,
|
|
1055
3723
|
busyMode: options.busyMode
|
|
1056
3724
|
});
|
|
3725
|
+
if (candidate.scope === "drive") {
|
|
3726
|
+
return dispatchDriveEnvelope(options, candidate, driveTransportFactory);
|
|
3727
|
+
}
|
|
3728
|
+
const input = buildUserInput(candidate, options.agentName, heartbeats);
|
|
3729
|
+
if (client.isWaitingOnApproval()) {
|
|
3730
|
+
dispatchLogger.warn("thread waiting on approval; skipping dispatch", {
|
|
3731
|
+
fileName: candidate.fileName,
|
|
3732
|
+
threadId: client.threadId,
|
|
3733
|
+
lastTurnStatus: client.lastTurnStatus
|
|
3734
|
+
});
|
|
3735
|
+
return false;
|
|
3736
|
+
}
|
|
1057
3737
|
if (client.isBusy()) {
|
|
1058
3738
|
if (options.busyMode !== "steer") {
|
|
1059
3739
|
dispatchLogger.debug("bridge busy and steer disabled", {
|
|
@@ -1076,8 +3756,10 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
|
1076
3756
|
candidate,
|
|
1077
3757
|
"steer",
|
|
1078
3758
|
client.threadId,
|
|
1079
|
-
turnId2
|
|
3759
|
+
turnId2,
|
|
3760
|
+
null
|
|
1080
3761
|
);
|
|
3762
|
+
markBridgeActivity();
|
|
1081
3763
|
dispatchLogger.info("steered active turn", {
|
|
1082
3764
|
fileName: candidate.fileName,
|
|
1083
3765
|
threadId: client.threadId,
|
|
@@ -1087,7 +3769,13 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
|
1087
3769
|
} catch (error) {
|
|
1088
3770
|
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
1089
3771
|
if (!client.isBusy()) {
|
|
1090
|
-
return dispatchCandidate(
|
|
3772
|
+
return dispatchCandidate(
|
|
3773
|
+
client,
|
|
3774
|
+
options,
|
|
3775
|
+
candidate,
|
|
3776
|
+
heartbeats,
|
|
3777
|
+
driveTransportFactory
|
|
3778
|
+
);
|
|
1091
3779
|
}
|
|
1092
3780
|
if (shouldRetrySteerAsStart(error)) {
|
|
1093
3781
|
client.activeTurnId = null;
|
|
@@ -1097,7 +3785,13 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
|
1097
3785
|
threadId: client.threadId,
|
|
1098
3786
|
error: sanitizeErrorForPersistence(String(error))
|
|
1099
3787
|
});
|
|
1100
|
-
return dispatchCandidate(
|
|
3788
|
+
return dispatchCandidate(
|
|
3789
|
+
client,
|
|
3790
|
+
options,
|
|
3791
|
+
candidate,
|
|
3792
|
+
heartbeats,
|
|
3793
|
+
driveTransportFactory
|
|
3794
|
+
);
|
|
1101
3795
|
}
|
|
1102
3796
|
throw error;
|
|
1103
3797
|
}
|
|
@@ -1115,8 +3809,10 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
|
1115
3809
|
candidate,
|
|
1116
3810
|
"start",
|
|
1117
3811
|
client.threadId,
|
|
1118
|
-
turnId
|
|
3812
|
+
turnId,
|
|
3813
|
+
null
|
|
1119
3814
|
);
|
|
3815
|
+
markBridgeActivity();
|
|
1120
3816
|
dispatchLogger.info("started turn for candidate", {
|
|
1121
3817
|
fileName: candidate.fileName,
|
|
1122
3818
|
threadId: client.threadId,
|
|
@@ -1152,7 +3848,7 @@ async function runScan(options, cutoff, client) {
|
|
|
1152
3848
|
candidate,
|
|
1153
3849
|
heartbeats
|
|
1154
3850
|
);
|
|
1155
|
-
if (!dispatched
|
|
3851
|
+
if (!dispatched) {
|
|
1156
3852
|
return { dispatched: false, maxMtimeMs };
|
|
1157
3853
|
}
|
|
1158
3854
|
maxMtimeMs = Math.max(maxMtimeMs, candidate.mtimeMs);
|
|
@@ -1165,6 +3861,7 @@ async function waitForTurnDrain(options, client, health) {
|
|
|
1165
3861
|
while (Date.now() < deadline) {
|
|
1166
3862
|
writeHeartbeat(options, client, health);
|
|
1167
3863
|
if (!client.activeTurnId) {
|
|
3864
|
+
markBridgeActivity();
|
|
1168
3865
|
return;
|
|
1169
3866
|
}
|
|
1170
3867
|
await delay(1e3);
|
|
@@ -1235,7 +3932,8 @@ async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
|
|
|
1235
3932
|
} catch (error) {
|
|
1236
3933
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1237
3934
|
throw new Error(
|
|
1238
|
-
`Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor
|
|
3935
|
+
`Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`,
|
|
3936
|
+
{ cause: error }
|
|
1239
3937
|
);
|
|
1240
3938
|
}
|
|
1241
3939
|
}
|
|
@@ -1272,7 +3970,11 @@ function formatJsonRpcError(error) {
|
|
|
1272
3970
|
2
|
|
1273
3971
|
);
|
|
1274
3972
|
}
|
|
3973
|
+
var DEFAULT_APP_SERVER_REQUEST_TIMEOUT_MS = 3e4;
|
|
1275
3974
|
var nextAppServerClientId = 1;
|
|
3975
|
+
function getProcessRssMb() {
|
|
3976
|
+
return Math.round(process.memoryUsage().rss / (1024 * 1024));
|
|
3977
|
+
}
|
|
1276
3978
|
var AppServerClient = class {
|
|
1277
3979
|
socket = null;
|
|
1278
3980
|
url;
|
|
@@ -1280,7 +3982,9 @@ var AppServerClient = class {
|
|
|
1280
3982
|
logger;
|
|
1281
3983
|
clientId = nextAppServerClientId++;
|
|
1282
3984
|
nextId = 1;
|
|
3985
|
+
requestTimeoutMs;
|
|
1283
3986
|
pending = /* @__PURE__ */ new Map();
|
|
3987
|
+
socketListeners = /* @__PURE__ */ new Map();
|
|
1284
3988
|
connected = false;
|
|
1285
3989
|
initialized = false;
|
|
1286
3990
|
threadId = null;
|
|
@@ -1293,10 +3997,14 @@ var AppServerClient = class {
|
|
|
1293
3997
|
lastError = null;
|
|
1294
3998
|
lastSuccessfulAppServerAt = null;
|
|
1295
3999
|
lastSuccessfulAppServerMethod = null;
|
|
1296
|
-
constructor(url, logger, gatewayToken) {
|
|
4000
|
+
constructor(url, logger, gatewayToken, requestTimeoutMs = DEFAULT_APP_SERVER_REQUEST_TIMEOUT_MS) {
|
|
1297
4001
|
this.url = url;
|
|
1298
4002
|
this.logger = logger;
|
|
1299
4003
|
this.gatewayToken = gatewayToken ?? null;
|
|
4004
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
4005
|
+
}
|
|
4006
|
+
getPendingRequestCount() {
|
|
4007
|
+
return this.pending.size;
|
|
1300
4008
|
}
|
|
1301
4009
|
async connect() {
|
|
1302
4010
|
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
|
@@ -1313,6 +4021,10 @@ var AppServerClient = class {
|
|
|
1313
4021
|
wsOptions.protocols = [`${AUTH_SUBPROTOCOL_PREFIX}${this.gatewayToken}`];
|
|
1314
4022
|
}
|
|
1315
4023
|
this.socket = new WebSocket(this.url, wsOptions);
|
|
4024
|
+
const socket = this.socket;
|
|
4025
|
+
if (!socket) {
|
|
4026
|
+
throw new Error(`Failed to create App Server socket for ${this.url}`);
|
|
4027
|
+
}
|
|
1316
4028
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
1317
4029
|
let settled = false;
|
|
1318
4030
|
const resolveOnce = () => {
|
|
@@ -1329,45 +4041,59 @@ var AppServerClient = class {
|
|
|
1329
4041
|
settled = true;
|
|
1330
4042
|
rejectPromise(error);
|
|
1331
4043
|
};
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
() => {
|
|
4044
|
+
const listeners = {
|
|
4045
|
+
open: () => {
|
|
1335
4046
|
this.connected = true;
|
|
1336
|
-
this.logger.info(
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
4047
|
+
this.logger.info(
|
|
4048
|
+
"connected to app-server",
|
|
4049
|
+
this.buildMetricsContext({
|
|
4050
|
+
url: this.url,
|
|
4051
|
+
authenticated: Boolean(this.gatewayToken)
|
|
4052
|
+
})
|
|
4053
|
+
);
|
|
1341
4054
|
resolveOnce();
|
|
1342
4055
|
},
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
4056
|
+
error: () => {
|
|
4057
|
+
const error = new Error(
|
|
4058
|
+
`Failed to connect to App Server at ${this.url}`
|
|
4059
|
+
);
|
|
4060
|
+
this.lastError = sanitizeErrorForPersistence(error.message);
|
|
4061
|
+
this.logger.error(
|
|
4062
|
+
"failed to connect to app-server",
|
|
4063
|
+
this.buildMetricsContext({
|
|
4064
|
+
url: this.url,
|
|
4065
|
+
error: this.lastError
|
|
4066
|
+
})
|
|
4067
|
+
);
|
|
4068
|
+
rejectOnce(error);
|
|
4069
|
+
},
|
|
4070
|
+
close: () => {
|
|
4071
|
+
this.connected = false;
|
|
4072
|
+
this.initialized = false;
|
|
4073
|
+
this.activeTurnId = null;
|
|
4074
|
+
this.turnStartedAt = null;
|
|
4075
|
+
this.detachSocketListeners(socket);
|
|
4076
|
+
if (this.socket === socket) {
|
|
4077
|
+
this.socket = null;
|
|
4078
|
+
}
|
|
4079
|
+
this.logger.warn(
|
|
4080
|
+
"disconnected from app-server",
|
|
4081
|
+
this.buildMetricsContext({
|
|
4082
|
+
url: this.url
|
|
4083
|
+
})
|
|
4084
|
+
);
|
|
4085
|
+
this.rejectPending(new Error("App Server connection closed"));
|
|
4086
|
+
},
|
|
4087
|
+
message: (event) => {
|
|
4088
|
+
const socketEvent = event;
|
|
4089
|
+
void this.handleMessage(socketEvent.data);
|
|
4090
|
+
}
|
|
4091
|
+
};
|
|
4092
|
+
this.socketListeners.set(socket, listeners);
|
|
4093
|
+
socket.addEventListener("open", listeners.open, { once: true });
|
|
4094
|
+
socket.addEventListener("error", listeners.error);
|
|
4095
|
+
socket.addEventListener("close", listeners.close);
|
|
4096
|
+
socket.addEventListener("message", listeners.message);
|
|
1371
4097
|
});
|
|
1372
4098
|
await this.request("initialize", {
|
|
1373
4099
|
clientInfo: {
|
|
@@ -1382,13 +4108,18 @@ var AppServerClient = class {
|
|
|
1382
4108
|
this.initialized = true;
|
|
1383
4109
|
}
|
|
1384
4110
|
async disconnect() {
|
|
1385
|
-
|
|
4111
|
+
const socket = this.socket;
|
|
4112
|
+
if (!socket) {
|
|
1386
4113
|
return;
|
|
1387
4114
|
}
|
|
1388
|
-
this.socket
|
|
4115
|
+
this.detachSocketListeners(socket);
|
|
4116
|
+
this.socket = null;
|
|
1389
4117
|
this.connected = false;
|
|
1390
4118
|
this.initialized = false;
|
|
1391
|
-
this.
|
|
4119
|
+
this.activeTurnId = null;
|
|
4120
|
+
this.turnStartedAt = null;
|
|
4121
|
+
this.rejectPending(new Error("App Server connection disconnected"));
|
|
4122
|
+
socket.close();
|
|
1392
4123
|
}
|
|
1393
4124
|
async ensureThread(explicitThreadId, savedThread, cwd, ephemeral) {
|
|
1394
4125
|
if (explicitThreadId) {
|
|
@@ -1416,10 +4147,6 @@ var AppServerClient = class {
|
|
|
1416
4147
|
);
|
|
1417
4148
|
}
|
|
1418
4149
|
}
|
|
1419
|
-
const loadedThreadId = await this.findLoadedThread(cwd);
|
|
1420
|
-
if (loadedThreadId) {
|
|
1421
|
-
return loadedThreadId;
|
|
1422
|
-
}
|
|
1423
4150
|
if (savedThread?.threadId) {
|
|
1424
4151
|
if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
|
|
1425
4152
|
this.logger.warn("saved thread cwd mismatch; skipping saved thread", {
|
|
@@ -1436,7 +4163,20 @@ var AppServerClient = class {
|
|
|
1436
4163
|
});
|
|
1437
4164
|
const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
|
|
1438
4165
|
await this.refreshThreadState(resumedThreadId);
|
|
1439
|
-
if (
|
|
4166
|
+
if (this.isWaitingOnApproval()) {
|
|
4167
|
+
this.logger.warn(
|
|
4168
|
+
"saved thread is waiting on approval; starting fresh thread",
|
|
4169
|
+
{
|
|
4170
|
+
clientId: this.clientId,
|
|
4171
|
+
threadId: resumedThreadId
|
|
4172
|
+
}
|
|
4173
|
+
);
|
|
4174
|
+
this.threadId = null;
|
|
4175
|
+
this.currentThreadCwd = null;
|
|
4176
|
+
this.activeTurnId = null;
|
|
4177
|
+
this.turnStartedAt = null;
|
|
4178
|
+
this.lastTurnStatus = null;
|
|
4179
|
+
} else if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
|
|
1440
4180
|
this.logger.warn("saved thread resumed with mismatched cwd", {
|
|
1441
4181
|
clientId: this.clientId,
|
|
1442
4182
|
threadId: resumedThreadId,
|
|
@@ -1468,6 +4208,10 @@ var AppServerClient = class {
|
|
|
1468
4208
|
}
|
|
1469
4209
|
}
|
|
1470
4210
|
}
|
|
4211
|
+
const loadedThreadId = await this.findLoadedThread(cwd);
|
|
4212
|
+
if (loadedThreadId) {
|
|
4213
|
+
return loadedThreadId;
|
|
4214
|
+
}
|
|
1471
4215
|
const startResponse = await this.request("thread/start", {
|
|
1472
4216
|
cwd,
|
|
1473
4217
|
ephemeral,
|
|
@@ -1480,7 +4224,7 @@ var AppServerClient = class {
|
|
|
1480
4224
|
}
|
|
1481
4225
|
this.syncThreadStateFromThread(startResponse?.thread);
|
|
1482
4226
|
this.threadId = startedThreadId;
|
|
1483
|
-
this.currentThreadCwd = this.currentThreadCwd ?? cwd;
|
|
4227
|
+
this.currentThreadCwd = this.currentThreadCwd ?? normalizePersistedThreadCwd(cwd);
|
|
1484
4228
|
this.activeTurnId = null;
|
|
1485
4229
|
this.lastTurnStatus = null;
|
|
1486
4230
|
this.logger.info("started thread", {
|
|
@@ -1592,6 +4336,9 @@ var AppServerClient = class {
|
|
|
1592
4336
|
}
|
|
1593
4337
|
return true;
|
|
1594
4338
|
}
|
|
4339
|
+
isWaitingOnApproval() {
|
|
4340
|
+
return isWaitingApprovalStatus(this.lastTurnStatus);
|
|
4341
|
+
}
|
|
1595
4342
|
async refreshCurrentThreadState() {
|
|
1596
4343
|
if (!this.threadId) {
|
|
1597
4344
|
return;
|
|
@@ -1621,7 +4368,7 @@ var AppServerClient = class {
|
|
|
1621
4368
|
if (typeof thread?.id === "string") {
|
|
1622
4369
|
this.threadId = thread.id;
|
|
1623
4370
|
}
|
|
1624
|
-
this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
|
|
4371
|
+
this.currentThreadCwd = typeof thread?.cwd === "string" ? normalizePersistedThreadCwd(thread.cwd) : null;
|
|
1625
4372
|
let activeTurnId = null;
|
|
1626
4373
|
let lastTurnStatus = null;
|
|
1627
4374
|
const threadActiveFlags = Array.isArray(
|
|
@@ -1629,6 +4376,7 @@ var AppServerClient = class {
|
|
|
1629
4376
|
) ? thread.status.activeFlags : [];
|
|
1630
4377
|
const threadStuckOnApproval = isTurnStuckOnApproval(threadActiveFlags);
|
|
1631
4378
|
if (threadStuckOnApproval) {
|
|
4379
|
+
lastTurnStatus = "waitingOnApproval";
|
|
1632
4380
|
this.logger.warn("thread waitingOnApproval; ignoring in-progress turns", {
|
|
1633
4381
|
clientId: this.clientId,
|
|
1634
4382
|
threadId: this.threadId
|
|
@@ -1645,6 +4393,7 @@ var AppServerClient = class {
|
|
|
1645
4393
|
}
|
|
1646
4394
|
const turnActiveFlags = Array.isArray(turn.activeFlags) ? turn.activeFlags : [];
|
|
1647
4395
|
if (isTurnStuckOnApproval(turnActiveFlags)) {
|
|
4396
|
+
lastTurnStatus = "waitingOnApproval";
|
|
1648
4397
|
this.logger.warn("turn waitingOnApproval; ignoring turn as active", {
|
|
1649
4398
|
clientId: this.clientId,
|
|
1650
4399
|
turnId: turn.id
|
|
@@ -1671,6 +4420,7 @@ var AppServerClient = class {
|
|
|
1671
4420
|
return;
|
|
1672
4421
|
}
|
|
1673
4422
|
this.pending.delete(message.id);
|
|
4423
|
+
this.clearPendingTimeout(pending);
|
|
1674
4424
|
if (message.error) {
|
|
1675
4425
|
const errorText = formatJsonRpcError(message.error);
|
|
1676
4426
|
this.lastError = sanitizeErrorForPersistence(errorText);
|
|
@@ -1688,6 +4438,31 @@ var AppServerClient = class {
|
|
|
1688
4438
|
this.lastError = null;
|
|
1689
4439
|
return;
|
|
1690
4440
|
}
|
|
4441
|
+
if ((typeof message.id === "number" || typeof message.id === "string") && typeof message.method === "string") {
|
|
4442
|
+
this.lastNotificationMethod = message.method;
|
|
4443
|
+
this.lastNotificationAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4444
|
+
if (isAutoElicitationRequestMethod(message.method)) {
|
|
4445
|
+
const result = buildAutoElicitationResult(message.params);
|
|
4446
|
+
if (result) {
|
|
4447
|
+
this.sendJsonRpcResult(message.id, result);
|
|
4448
|
+
this.logger.info("auto-responded to elicitation request", {
|
|
4449
|
+
clientId: this.clientId,
|
|
4450
|
+
method: message.method,
|
|
4451
|
+
action: result.action
|
|
4452
|
+
});
|
|
4453
|
+
} else {
|
|
4454
|
+
this.sendJsonRpcResult(message.id, { action: "cancel" });
|
|
4455
|
+
this.logger.warn(
|
|
4456
|
+
"elicitation request missing usable params; cancelled",
|
|
4457
|
+
{
|
|
4458
|
+
clientId: this.clientId,
|
|
4459
|
+
method: message.method
|
|
4460
|
+
}
|
|
4461
|
+
);
|
|
4462
|
+
}
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
1691
4466
|
if (!message.method) {
|
|
1692
4467
|
return;
|
|
1693
4468
|
}
|
|
@@ -1701,12 +4476,24 @@ var AppServerClient = class {
|
|
|
1701
4476
|
}
|
|
1702
4477
|
handleNotification(method, params) {
|
|
1703
4478
|
switch (method) {
|
|
4479
|
+
case "notifications/claude/channel":
|
|
4480
|
+
this.logger.info("tap channel notification received", {
|
|
4481
|
+
clientId: this.clientId,
|
|
4482
|
+
source: params?.meta?.source ?? null,
|
|
4483
|
+
from: params?.meta?.from ?? null,
|
|
4484
|
+
to: params?.meta?.to ?? null,
|
|
4485
|
+
subject: params?.meta?.subject ?? null,
|
|
4486
|
+
filename: params?.meta?.filename ?? null
|
|
4487
|
+
});
|
|
4488
|
+
break;
|
|
1704
4489
|
case "thread/started":
|
|
1705
4490
|
if (params?.thread?.id) {
|
|
1706
4491
|
this.threadId = params.thread.id;
|
|
1707
4492
|
}
|
|
1708
4493
|
if (typeof params?.thread?.cwd === "string") {
|
|
1709
|
-
this.currentThreadCwd =
|
|
4494
|
+
this.currentThreadCwd = normalizePersistedThreadCwd(
|
|
4495
|
+
params.thread.cwd
|
|
4496
|
+
);
|
|
1710
4497
|
}
|
|
1711
4498
|
this.logger.info("thread started notification", {
|
|
1712
4499
|
clientId: this.clientId,
|
|
@@ -1756,11 +4543,16 @@ var AppServerClient = class {
|
|
|
1756
4543
|
});
|
|
1757
4544
|
break;
|
|
1758
4545
|
default:
|
|
4546
|
+
this.logger.info("unhandled app-server notification", {
|
|
4547
|
+
clientId: this.clientId,
|
|
4548
|
+
method
|
|
4549
|
+
});
|
|
1759
4550
|
break;
|
|
1760
4551
|
}
|
|
1761
4552
|
}
|
|
1762
4553
|
request(method, params) {
|
|
1763
|
-
|
|
4554
|
+
const socket = this.socket;
|
|
4555
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
1764
4556
|
throw new Error(`Cannot call ${method}; App Server socket is not open`);
|
|
1765
4557
|
}
|
|
1766
4558
|
const id = this.nextId;
|
|
@@ -1772,34 +4564,106 @@ var AppServerClient = class {
|
|
|
1772
4564
|
params
|
|
1773
4565
|
};
|
|
1774
4566
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
4567
|
+
const timeout = setTimeout(() => {
|
|
4568
|
+
const pending = this.pending.get(id);
|
|
4569
|
+
if (!pending) {
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
this.pending.delete(id);
|
|
4573
|
+
const errorText = `${method} timed out after ${this.requestTimeoutMs}ms`;
|
|
4574
|
+
this.lastError = sanitizeErrorForPersistence(errorText);
|
|
4575
|
+
this.logger.warn(
|
|
4576
|
+
"app-server request timed out",
|
|
4577
|
+
this.buildMetricsContext({
|
|
4578
|
+
method,
|
|
4579
|
+
requestId: id,
|
|
4580
|
+
timeoutMs: this.requestTimeoutMs
|
|
4581
|
+
})
|
|
4582
|
+
);
|
|
4583
|
+
pending.reject(new Error(errorText));
|
|
4584
|
+
}, this.requestTimeoutMs);
|
|
1775
4585
|
this.pending.set(id, {
|
|
1776
4586
|
resolve: resolvePromise,
|
|
1777
4587
|
reject: rejectPromise,
|
|
1778
|
-
method
|
|
4588
|
+
method,
|
|
4589
|
+
timeout
|
|
1779
4590
|
});
|
|
1780
|
-
|
|
4591
|
+
try {
|
|
4592
|
+
socket.send(JSON.stringify(request));
|
|
4593
|
+
} catch (error) {
|
|
4594
|
+
const pending = this.pending.get(id);
|
|
4595
|
+
if (pending) {
|
|
4596
|
+
this.clearPendingTimeout(pending);
|
|
4597
|
+
this.pending.delete(id);
|
|
4598
|
+
}
|
|
4599
|
+
rejectPromise(error);
|
|
4600
|
+
}
|
|
1781
4601
|
});
|
|
1782
4602
|
}
|
|
4603
|
+
sendJsonRpcResult(id, result) {
|
|
4604
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
4605
|
+
return;
|
|
4606
|
+
}
|
|
4607
|
+
this.socket.send(JSON.stringify({ jsonrpc: "2.0", id, result }));
|
|
4608
|
+
}
|
|
1783
4609
|
rejectPending(error) {
|
|
4610
|
+
if (this.pending.size > 0) {
|
|
4611
|
+
this.logger.warn(
|
|
4612
|
+
"rejecting pending app-server requests",
|
|
4613
|
+
this.buildMetricsContext({
|
|
4614
|
+
error: sanitizeErrorForPersistence(error.message)
|
|
4615
|
+
})
|
|
4616
|
+
);
|
|
4617
|
+
}
|
|
1784
4618
|
for (const pending of this.pending.values()) {
|
|
4619
|
+
this.clearPendingTimeout(pending);
|
|
1785
4620
|
pending.reject(error);
|
|
1786
4621
|
}
|
|
1787
4622
|
this.pending.clear();
|
|
1788
4623
|
}
|
|
4624
|
+
clearPendingTimeout(pending) {
|
|
4625
|
+
if (pending.timeout !== null) {
|
|
4626
|
+
clearTimeout(pending.timeout);
|
|
4627
|
+
pending.timeout = null;
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
detachSocketListeners(socket) {
|
|
4631
|
+
const listeners = this.socketListeners.get(socket);
|
|
4632
|
+
if (!listeners) {
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
socket.removeEventListener("open", listeners.open);
|
|
4636
|
+
socket.removeEventListener("error", listeners.error);
|
|
4637
|
+
socket.removeEventListener("close", listeners.close);
|
|
4638
|
+
socket.removeEventListener("message", listeners.message);
|
|
4639
|
+
this.socketListeners.delete(socket);
|
|
4640
|
+
}
|
|
4641
|
+
buildMetricsContext(context) {
|
|
4642
|
+
return {
|
|
4643
|
+
clientId: this.clientId,
|
|
4644
|
+
reconnectCount: Math.max(this.clientId - 1, 0),
|
|
4645
|
+
pendingCount: this.pending.size,
|
|
4646
|
+
rssMb: getProcessRssMb(),
|
|
4647
|
+
...context
|
|
4648
|
+
};
|
|
4649
|
+
}
|
|
1789
4650
|
};
|
|
1790
4651
|
|
|
1791
4652
|
// scripts/bridge/bridge-main.ts
|
|
1792
|
-
import { existsSync as
|
|
1793
|
-
import { isAbsolute as
|
|
4653
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4654
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve5 } from "path";
|
|
1794
4655
|
import { pathToFileURL } from "url";
|
|
1795
4656
|
function delay2(ms) {
|
|
1796
4657
|
return new Promise((resolvePromise) => {
|
|
1797
4658
|
setTimeout(resolvePromise, ms);
|
|
1798
4659
|
});
|
|
1799
4660
|
}
|
|
4661
|
+
function getProcessRssMb2() {
|
|
4662
|
+
return Math.round(process.memoryUsage().rss / (1024 * 1024));
|
|
4663
|
+
}
|
|
1800
4664
|
function readHeartbeatState(stateDir) {
|
|
1801
|
-
const heartbeatPath =
|
|
1802
|
-
if (!
|
|
4665
|
+
const heartbeatPath = join8(stateDir, "heartbeat.json");
|
|
4666
|
+
if (!existsSync7(heartbeatPath)) {
|
|
1803
4667
|
return null;
|
|
1804
4668
|
}
|
|
1805
4669
|
try {
|
|
@@ -1823,7 +4687,7 @@ function hasValidHeartbeatThreadCwd(threadCwd) {
|
|
|
1823
4687
|
if (!normalized) {
|
|
1824
4688
|
return false;
|
|
1825
4689
|
}
|
|
1826
|
-
return
|
|
4690
|
+
return isAbsolute2(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
|
|
1827
4691
|
}
|
|
1828
4692
|
function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
|
|
1829
4693
|
const savedThread = readThreadState(stateDir);
|
|
@@ -1843,7 +4707,9 @@ function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
|
|
|
1843
4707
|
updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1844
4708
|
appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
|
|
1845
4709
|
ephemeral: savedThread?.ephemeral ?? false,
|
|
1846
|
-
cwd:
|
|
4710
|
+
cwd: normalizePersistedThreadCwd(
|
|
4711
|
+
heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
|
|
4712
|
+
)
|
|
1847
4713
|
};
|
|
1848
4714
|
let preferred = savedThread;
|
|
1849
4715
|
if (!savedThread?.threadId) {
|
|
@@ -1853,7 +4719,9 @@ function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
|
|
|
1853
4719
|
...savedThread,
|
|
1854
4720
|
updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
|
|
1855
4721
|
appServerUrl: heartbeatBackedThread.appServerUrl,
|
|
1856
|
-
cwd:
|
|
4722
|
+
cwd: normalizePersistedThreadCwd(
|
|
4723
|
+
heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
|
|
4724
|
+
)
|
|
1857
4725
|
};
|
|
1858
4726
|
} else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
|
|
1859
4727
|
preferred = heartbeatBackedThread;
|
|
@@ -1865,8 +4733,8 @@ function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessage
|
|
|
1865
4733
|
return /* @__PURE__ */ new Date(0);
|
|
1866
4734
|
}
|
|
1867
4735
|
const lookbackCutoff = lookbackMinutes > 0 ? new Date(Date.now() - lookbackMinutes * 6e4) : null;
|
|
1868
|
-
const cutoffPath =
|
|
1869
|
-
if (
|
|
4736
|
+
const cutoffPath = join8(stateDir, "general-inbox-cutoff.txt");
|
|
4737
|
+
if (existsSync7(cutoffPath)) {
|
|
1870
4738
|
try {
|
|
1871
4739
|
const saved = new Date(readFileSync6(cutoffPath, "utf8").trim());
|
|
1872
4740
|
if (!isNaN(saved.getTime())) {
|
|
@@ -1882,7 +4750,7 @@ function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessage
|
|
|
1882
4750
|
return lookbackCutoff;
|
|
1883
4751
|
}
|
|
1884
4752
|
const cutoff = /* @__PURE__ */ new Date();
|
|
1885
|
-
|
|
4753
|
+
writeFileSync6(cutoffPath, `${cutoff.toISOString()}
|
|
1886
4754
|
`, "utf8");
|
|
1887
4755
|
return cutoff;
|
|
1888
4756
|
}
|
|
@@ -1890,6 +4758,18 @@ async function main() {
|
|
|
1890
4758
|
const options = buildOptions(process.argv.slice(2));
|
|
1891
4759
|
configureBridgeLogging(options.logLevel);
|
|
1892
4760
|
const logger = createBridgeLogger("bridge");
|
|
4761
|
+
const sweepLogger = createBridgeLogger("sweep");
|
|
4762
|
+
const sweepResult = sweepOrphanProcessedMarkers(options.stateDir, {
|
|
4763
|
+
logger: (msg, ctx) => sweepLogger.debug(msg, ctx)
|
|
4764
|
+
});
|
|
4765
|
+
if (sweepResult.scanned > 0) {
|
|
4766
|
+
logger.info("processed marker sweep", {
|
|
4767
|
+
scanned: sweepResult.scanned,
|
|
4768
|
+
removed: sweepResult.removed,
|
|
4769
|
+
kept: sweepResult.kept,
|
|
4770
|
+
errors: sweepResult.errors
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
1893
4773
|
const cutoff = getGeneralInboxCutoff(
|
|
1894
4774
|
options.stateDir,
|
|
1895
4775
|
options.messageLookbackMinutes,
|
|
@@ -1962,9 +4842,9 @@ async function main() {
|
|
|
1962
4842
|
}
|
|
1963
4843
|
const scanResult = await runScan(options, cutoff, client);
|
|
1964
4844
|
if (scanResult.dispatched && scanResult.maxMtimeMs > 0) {
|
|
1965
|
-
const cutoffPath =
|
|
4845
|
+
const cutoffPath = join8(options.stateDir, "general-inbox-cutoff.txt");
|
|
1966
4846
|
const advancedCutoff = new Date(scanResult.maxMtimeMs);
|
|
1967
|
-
|
|
4847
|
+
writeFileSync6(cutoffPath, `${advancedCutoff.toISOString()}
|
|
1968
4848
|
`, "utf8");
|
|
1969
4849
|
}
|
|
1970
4850
|
if (scanResult.dispatched && client && options.waitAfterDispatchSeconds > 0) {
|
|
@@ -1990,11 +4870,15 @@ async function main() {
|
|
|
1990
4870
|
const sanitized = sanitizeErrorForPersistence(message);
|
|
1991
4871
|
throw new Error(sanitized ?? message);
|
|
1992
4872
|
}
|
|
4873
|
+
const pendingCount = client?.getPendingRequestCount() ?? 0;
|
|
1993
4874
|
client?.disconnect().catch(() => void 0);
|
|
1994
4875
|
client = null;
|
|
1995
4876
|
logger.warn("reconnecting after bridge error", {
|
|
1996
4877
|
reconnectSeconds: options.reconnectSeconds,
|
|
1997
|
-
|
|
4878
|
+
reconnectCount: health.consecutiveFailureCount,
|
|
4879
|
+
consecutiveFailureCount: health.consecutiveFailureCount,
|
|
4880
|
+
pendingCount,
|
|
4881
|
+
rssMb: getProcessRssMb2()
|
|
1998
4882
|
});
|
|
1999
4883
|
await delay2(options.reconnectSeconds * 1e3);
|
|
2000
4884
|
}
|
|
@@ -2004,14 +4888,15 @@ async function main() {
|
|
|
2004
4888
|
function isDirectExecution() {
|
|
2005
4889
|
const entry = process.argv[1];
|
|
2006
4890
|
if (!entry) return false;
|
|
2007
|
-
return import.meta.url === pathToFileURL(
|
|
4891
|
+
return import.meta.url === pathToFileURL(resolve5(entry)).href;
|
|
2008
4892
|
}
|
|
2009
4893
|
|
|
2010
4894
|
// src/bridges/codex-app-server-bridge.ts
|
|
2011
4895
|
function isDirectExecution2() {
|
|
2012
4896
|
const entry = process.argv[1];
|
|
2013
4897
|
if (!entry) return false;
|
|
2014
|
-
|
|
4898
|
+
if (!basename2(entry).startsWith("codex-app-server-bridge")) return false;
|
|
4899
|
+
return import.meta.url === pathToFileURL2(resolve6(entry)).href;
|
|
2015
4900
|
}
|
|
2016
4901
|
if (isDirectExecution2()) {
|
|
2017
4902
|
main().catch((error) => {
|
|
@@ -2027,7 +4912,11 @@ export {
|
|
|
2027
4912
|
COMMS_HEARTBEAT_LOCK_TIMEOUT_MS,
|
|
2028
4913
|
COMMS_LOCK_STALE_AGE_MS,
|
|
2029
4914
|
DEFAULT_AGENT,
|
|
4915
|
+
DEFAULT_APP_SERVER_REQUEST_TIMEOUT_MS,
|
|
2030
4916
|
DEFAULT_APP_SERVER_URL,
|
|
4917
|
+
DRIVE_ACTION_NOT_YET_SUPPORTED_REASON,
|
|
4918
|
+
DRIVE_NOT_YET_WIRED_REASON,
|
|
4919
|
+
FORBIDDEN_RAW_PAIR_TOKEN_REASON,
|
|
2031
4920
|
HEADLESS_SKIP_PATTERNS,
|
|
2032
4921
|
HEADLESS_WARMUP_PROMPT,
|
|
2033
4922
|
HEADLESS_WARMUP_TIMEOUT_MS,
|
|
@@ -2036,6 +4925,7 @@ export {
|
|
|
2036
4925
|
TURN_COMPLETION_POLL_MS,
|
|
2037
4926
|
TURN_COMPLETION_REFRESH_MS,
|
|
2038
4927
|
acquireCommsLock,
|
|
4928
|
+
buildAutoElicitationResult,
|
|
2039
4929
|
buildDefaultStateDir,
|
|
2040
4930
|
buildMarkerId,
|
|
2041
4931
|
buildOptions,
|
|
@@ -2049,17 +4939,23 @@ export {
|
|
|
2049
4939
|
getGeneralInboxCutoff,
|
|
2050
4940
|
getInboxRoute,
|
|
2051
4941
|
getInboxRouteFromFilename,
|
|
4942
|
+
getLastBridgeActivityAt,
|
|
2052
4943
|
getPendingCandidates,
|
|
2053
4944
|
getProcessedMarkerPath,
|
|
4945
|
+
isAutoElicitationRequestMethod,
|
|
2054
4946
|
isDirectExecution,
|
|
2055
4947
|
isOwnMessageSender,
|
|
2056
4948
|
isTurnStale,
|
|
2057
4949
|
isTurnStuckOnApproval,
|
|
4950
|
+
isWaitingApprovalStatus,
|
|
2058
4951
|
loadHeartbeats,
|
|
2059
4952
|
loadResumableThreadState,
|
|
2060
4953
|
main,
|
|
4954
|
+
markBridgeActivity,
|
|
2061
4955
|
maybeBootstrapHeadlessTurn,
|
|
2062
4956
|
normalizeAgentToken,
|
|
4957
|
+
normalizePersistedThreadCwd,
|
|
4958
|
+
normalizeRoutingSlotEnv,
|
|
2063
4959
|
normalizeThreadCwd,
|
|
2064
4960
|
parseArgs,
|
|
2065
4961
|
parseBridgeFrontmatter,
|
|
@@ -2087,6 +4983,8 @@ export {
|
|
|
2087
4983
|
shouldRetrySteerAsStart,
|
|
2088
4984
|
shouldSkipInHeadlessMode,
|
|
2089
4985
|
stripBridgeFrontmatter,
|
|
4986
|
+
stripWindowsNamespacePrefix,
|
|
4987
|
+
sweepOrphanProcessedMarkers,
|
|
2090
4988
|
threadCwdMatches,
|
|
2091
4989
|
updateCommsHeartbeat,
|
|
2092
4990
|
waitForTurnCompletion,
|