@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.
Files changed (47) hide show
  1. package/AI_GUIDE.md +165 -0
  2. package/CHANGELOG.md +67 -0
  3. package/README.md +204 -18
  4. package/dist/bridges/codex-app-server-auth-gateway.mjs +16 -1
  5. package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
  6. package/dist/bridges/codex-app-server-bridge.d.mts +105 -12
  7. package/dist/bridges/codex-app-server-bridge.mjs +3149 -251
  8. package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
  9. package/dist/bridges/codex-bridge-runner.d.mts +4 -1
  10. package/dist/bridges/codex-bridge-runner.mjs +512 -58
  11. package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
  12. package/dist/bridges/codex-remote-ipc-relay.d.mts +1 -0
  13. package/dist/bridges/codex-remote-ipc-relay.mjs +1912 -0
  14. package/dist/bridges/codex-remote-ipc-relay.mjs.map +1 -0
  15. package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -1
  16. package/dist/cli.mjs +30818 -8324
  17. package/dist/cli.mjs.map +1 -1
  18. package/dist/codex-a2a/index.d.mts +2 -0
  19. package/dist/codex-a2a/index.mjs +416 -0
  20. package/dist/codex-a2a/index.mjs.map +1 -0
  21. package/dist/codex-health/index.d.mts +76 -0
  22. package/dist/codex-health/index.mjs +153 -0
  23. package/dist/codex-health/index.mjs.map +1 -0
  24. package/dist/codex-ipc/index.d.mts +2 -0
  25. package/dist/codex-ipc/index.mjs +1834 -0
  26. package/dist/codex-ipc/index.mjs.map +1 -0
  27. package/dist/index-D4Khz2Mh.d.mts +206 -0
  28. package/dist/index-DMToLyGd.d.mts +256 -0
  29. package/dist/index.d.mts +763 -8
  30. package/dist/index.mjs +11586 -3438
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/mcp-server.mjs +8838 -811
  33. package/dist/mcp-server.mjs.map +1 -1
  34. package/dist/types-FWvKrFUt.d.mts +43 -0
  35. package/examples/01-logic-battle-known-broken.md +46 -0
  36. package/examples/02-cross-model-review-root-cause.md +37 -0
  37. package/examples/03-convergence-pattern.md +42 -0
  38. package/examples/04-tower-broadcast.md +41 -0
  39. package/examples/05-self-awareness-paradox.md +49 -0
  40. package/examples/06-session-resurrection.md +37 -0
  41. package/examples/07-ghost-agent.md +31 -0
  42. package/examples/08-naming-creates-identity.md +36 -0
  43. package/examples/09-ceo-as-middleware.md +52 -0
  44. package/examples/10-files-as-interface.md +67 -0
  45. package/examples/README.md +34 -0
  46. package/examples/tap-profile-pack.example.json +71 -0
  47. package/package.json +21 -3
@@ -0,0 +1,1912 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bridges/codex-remote-ipc-relay.ts
4
+ import { createHash as createHash2 } from "crypto";
5
+
6
+ // src/transport/experimental/codex-ipc-control.ts
7
+ import { randomUUID as randomUUID4 } from "crypto";
8
+
9
+ // src/transport/consent.ts
10
+ import { createHash, randomBytes, randomUUID } from "crypto";
11
+ import { execFileSync } from "child_process";
12
+ import * as fs from "fs";
13
+ import * as os from "os";
14
+ import * as path from "path";
15
+ var CONSENT_RECEIPTS_DIRNAME = "tap-codex-a2a-consent";
16
+ var CONSENT_SECRETS_DIRNAME = "tap-codex-a2a-consent-secrets";
17
+ var DEFAULT_CONSENT_TTL_SECONDS = 10 * 60;
18
+ var CONSENT_METADATA_DRIFT_TOLERANCE_MS = 5e3;
19
+ var CONSENT_RESERVATION_TTL_MS = 3e4;
20
+ var pendingConsentReservations = /* @__PURE__ */ new Set();
21
+ var SCOPE_PRIORITY = {
22
+ observe: 1,
23
+ suggest: 2,
24
+ drive: 3
25
+ };
26
+ var ConsentReceiptError = class extends Error {
27
+ constructor(code, message) {
28
+ super(message);
29
+ this.code = code;
30
+ this.name = "ConsentReceiptError";
31
+ }
32
+ code;
33
+ };
34
+ function normalizeString(value) {
35
+ const normalized = value?.trim();
36
+ return normalized ? normalized : null;
37
+ }
38
+ function assertPendingReservationAvailable(consentRef) {
39
+ if (!pendingConsentReservations.has(consentRef)) {
40
+ return;
41
+ }
42
+ throw new ConsentReceiptError(
43
+ "missing",
44
+ `Consent receipt "${consentRef}" is already reserved or consumed.`
45
+ );
46
+ }
47
+ function markPendingReservation(consentRef) {
48
+ pendingConsentReservations.add(consentRef);
49
+ }
50
+ function clearPendingReservation(consentRef) {
51
+ pendingConsentReservations.delete(consentRef);
52
+ }
53
+ function normalizeMethods(values) {
54
+ const methods = /* @__PURE__ */ new Set();
55
+ for (const value of values ?? []) {
56
+ const normalized = value.trim();
57
+ if (!normalized) continue;
58
+ methods.add(normalized);
59
+ }
60
+ return [...methods].sort();
61
+ }
62
+ function normalizePathForComparison(value) {
63
+ return path.resolve(value).replace(/\\/g, "/").toLowerCase();
64
+ }
65
+ function resolveReceiptsDir(explicitDir) {
66
+ const configuredDir = explicitDir?.trim() || process.env.TAP_CONSENT_RECEIPTS_DIR?.trim();
67
+ return configuredDir ? path.resolve(configuredDir) : path.join(os.tmpdir(), CONSENT_RECEIPTS_DIRNAME);
68
+ }
69
+ function resolveSecretsDir(explicitDir) {
70
+ const configuredDir = explicitDir?.trim() || process.env.TAP_CONSENT_SECRETS_DIR?.trim();
71
+ return configuredDir ? path.resolve(configuredDir) : path.join(os.tmpdir(), CONSENT_SECRETS_DIRNAME);
72
+ }
73
+ function resolveConsentDirs(options) {
74
+ const receiptsDir = resolveReceiptsDir(options.receiptsDir);
75
+ const secretsDir = resolveSecretsDir(options.secretsDir);
76
+ if (normalizePathForComparison(receiptsDir) === normalizePathForComparison(secretsDir)) {
77
+ throw new ConsentReceiptError(
78
+ "invalid",
79
+ "Consent receipts dir and secrets dir must be different paths."
80
+ );
81
+ }
82
+ return { receiptsDir, secretsDir };
83
+ }
84
+ function hashPairTokenBinding(options) {
85
+ return createHash("sha256").update(
86
+ [
87
+ options.pairToken,
88
+ options.hostId ?? "",
89
+ options.conversationId,
90
+ options.ownerClientId ?? ""
91
+ ].join("\0"),
92
+ "utf-8"
93
+ ).digest("hex");
94
+ }
95
+ function readUtf8PreservingTimes(filePath) {
96
+ const originalStats = fs.statSync(filePath);
97
+ const contents = fs.readFileSync(filePath, "utf-8");
98
+ try {
99
+ fs.utimesSync(filePath, originalStats.atime, originalStats.mtime);
100
+ } catch {
101
+ }
102
+ return contents;
103
+ }
104
+ function loadConsentReceipt(filePath) {
105
+ try {
106
+ const parsed = JSON.parse(
107
+ readUtf8PreservingTimes(filePath)
108
+ );
109
+ 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") {
110
+ return null;
111
+ }
112
+ if (parsed.scope !== "observe" && parsed.scope !== "suggest" && parsed.scope !== "drive") {
113
+ return null;
114
+ }
115
+ return {
116
+ id: parsed.id,
117
+ scope: parsed.scope,
118
+ hostId: normalizeString(parsed.hostId),
119
+ conversationId: parsed.conversationId,
120
+ ownerClientId: normalizeString(parsed.ownerClientId),
121
+ issuedByClientId: normalizeString(parsed.issuedByClientId),
122
+ allowedMethods: normalizeMethods(parsed.allowedMethods),
123
+ pairTokenHash: parsed.pairTokenHash,
124
+ createdAt: parsed.createdAt,
125
+ expiresAt: parsed.expiresAt
126
+ };
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ function loadReservedReceiptRecord(filePath) {
132
+ try {
133
+ const parsed = JSON.parse(readUtf8PreservingTimes(filePath));
134
+ return {
135
+ receipt: loadConsentReceipt(filePath),
136
+ reservationOwnerId: normalizeString(parsed.reservationOwnerId)
137
+ };
138
+ } catch {
139
+ return {
140
+ receipt: null,
141
+ reservationOwnerId: null
142
+ };
143
+ }
144
+ }
145
+ function isExpired(receipt, now) {
146
+ const expiresAtMs = new Date(receipt.expiresAt).getTime();
147
+ return Number.isNaN(expiresAtMs) || expiresAtMs <= now.getTime();
148
+ }
149
+ function resolveSecretPath(secretsDir, receiptId) {
150
+ return path.join(secretsDir, `${receiptId}.token`);
151
+ }
152
+ function resolveReservedReceiptPath(receiptsDir, receiptId) {
153
+ return path.join(receiptsDir, `${receiptId}.reserved.json`);
154
+ }
155
+ function extractReceiptIdFromPath(filePath) {
156
+ return path.basename(filePath).replace(/(?:\.reserved)?\.json$/i, "");
157
+ }
158
+ function isReceiptPath(fileName) {
159
+ return /\.json$/i.test(fileName);
160
+ }
161
+ function resolveWindowsAclPrincipals() {
162
+ const username = process.env.USERNAME?.trim();
163
+ if (!username) return [];
164
+ const principals = /* @__PURE__ */ new Set();
165
+ const userDomain = process.env.USERDOMAIN?.trim();
166
+ if (userDomain) {
167
+ principals.add(`${userDomain}\\${username}`);
168
+ }
169
+ principals.add(username);
170
+ return [...principals];
171
+ }
172
+ function applyWindowsPrivateAcl(targetPath) {
173
+ if (process.platform !== "win32") return;
174
+ const principals = resolveWindowsAclPrincipals();
175
+ if (principals.length === 0) {
176
+ throw new ConsentReceiptError(
177
+ "invalid",
178
+ `Unable to resolve a Windows principal for "${path.basename(targetPath)}".`
179
+ );
180
+ }
181
+ let lastError = null;
182
+ for (const principal of principals) {
183
+ try {
184
+ execFileSync(
185
+ "icacls",
186
+ [targetPath, "/inheritance:r", "/grant:r", `${principal}:F`],
187
+ {
188
+ stdio: "pipe",
189
+ windowsHide: true
190
+ }
191
+ );
192
+ return;
193
+ } catch (error) {
194
+ lastError = error;
195
+ }
196
+ }
197
+ throw new ConsentReceiptError(
198
+ "invalid",
199
+ `Failed to apply Windows ACL hardening to "${path.basename(targetPath)}": ${lastError instanceof Error ? lastError.message : String(lastError)}`
200
+ );
201
+ }
202
+ function hardenSecretStorePath(targetPath, mode) {
203
+ try {
204
+ fs.chmodSync(targetPath, mode);
205
+ } catch {
206
+ }
207
+ applyWindowsPrivateAcl(targetPath);
208
+ }
209
+ function hasTimestampDrift(stats, mintedAtMs) {
210
+ if (!Number.isFinite(mintedAtMs)) {
211
+ return false;
212
+ }
213
+ return Math.abs(stats.mtimeMs - mintedAtMs) > CONSENT_METADATA_DRIFT_TOLERANCE_MS || Math.abs(stats.atimeMs - mintedAtMs) > CONSENT_METADATA_DRIFT_TOLERANCE_MS;
214
+ }
215
+ function stampMintedAt(targetPath, mintedAt) {
216
+ fs.utimesSync(targetPath, mintedAt, mintedAt);
217
+ }
218
+ function stampReservationAt(targetPath, reservedAt) {
219
+ fs.utimesSync(targetPath, reservedAt, reservedAt);
220
+ }
221
+ function resolveReceiptCreatedAtMs(receipt) {
222
+ const createdAtMs = new Date(receipt.createdAt).getTime();
223
+ if (Number.isNaN(createdAtMs)) {
224
+ throw new ConsentReceiptError(
225
+ "invalid",
226
+ `Consent receipt "${receipt.id}" has an invalid createdAt timestamp.`
227
+ );
228
+ }
229
+ return createdAtMs;
230
+ }
231
+ function resolveReceiptCreatedAt(receipt) {
232
+ return new Date(resolveReceiptCreatedAtMs(receipt));
233
+ }
234
+ function isReservationExpired(stats, now) {
235
+ return now.getTime() - stats.mtimeMs > CONSENT_RESERVATION_TTL_MS;
236
+ }
237
+ function assertUntamperedConsentPath(stats, receipt, label) {
238
+ if (!hasTimestampDrift(stats, resolveReceiptCreatedAtMs(receipt))) {
239
+ return;
240
+ }
241
+ throw new ConsentReceiptError(
242
+ "invalid",
243
+ `Consent ${label} "${receipt.id}" showed timestamp drift after mint.`
244
+ );
245
+ }
246
+ function removeSecretPath(secretPath) {
247
+ try {
248
+ fs.rmSync(secretPath, { force: true });
249
+ } catch {
250
+ }
251
+ }
252
+ function removeReceiptPath(receiptPath) {
253
+ try {
254
+ fs.rmSync(receiptPath, { force: true });
255
+ } catch {
256
+ }
257
+ }
258
+ function writeActiveReceiptFile(filePath, receipt) {
259
+ fs.writeFileSync(filePath, JSON.stringify(receipt, null, 2), "utf-8");
260
+ stampMintedAt(filePath, resolveReceiptCreatedAt(receipt));
261
+ }
262
+ function writeReservedReceiptFile(filePath, receipt, reservationOwnerId, reservedAt) {
263
+ fs.writeFileSync(
264
+ filePath,
265
+ JSON.stringify(
266
+ {
267
+ ...receipt,
268
+ reservationOwnerId
269
+ },
270
+ null,
271
+ 2
272
+ ),
273
+ "utf-8"
274
+ );
275
+ stampReservationAt(filePath, reservedAt);
276
+ }
277
+ function cleanupExpiredReceipts(receiptsDir, secretsDir, now) {
278
+ if (!fs.existsSync(receiptsDir)) return;
279
+ for (const entry of fs.readdirSync(receiptsDir, { withFileTypes: true })) {
280
+ if (!entry.isFile() || !isReceiptPath(entry.name)) continue;
281
+ const filePath = path.join(receiptsDir, entry.name);
282
+ const receipt = loadConsentReceipt(filePath);
283
+ const receiptId = receipt?.id ?? extractReceiptIdFromPath(filePath);
284
+ if (!receipt || isExpired(receipt, now)) {
285
+ removeReceiptPath(filePath);
286
+ removeSecretPath(resolveSecretPath(secretsDir, receiptId));
287
+ }
288
+ }
289
+ }
290
+ function listReceiptPaths(receiptsDir) {
291
+ if (!fs.existsSync(receiptsDir)) return [];
292
+ return fs.readdirSync(receiptsDir, { withFileTypes: true }).filter(
293
+ (entry) => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".reserved.json")
294
+ ).map((entry) => path.join(receiptsDir, entry.name)).sort();
295
+ }
296
+ function scopeSatisfies(actual, required) {
297
+ return SCOPE_PRIORITY[actual] >= SCOPE_PRIORITY[required];
298
+ }
299
+ function resolveReceiptPath(receiptsDir, consentRef) {
300
+ const normalizedConsentRef = normalizeString(consentRef);
301
+ if (!normalizedConsentRef) return null;
302
+ return path.join(receiptsDir, `${normalizedConsentRef}.json`);
303
+ }
304
+ function reserveReceiptPath(filePath, receipt, reservationOwnerId, now) {
305
+ const reservedPath = resolveReservedReceiptPath(
306
+ path.dirname(filePath),
307
+ receipt.id
308
+ );
309
+ try {
310
+ fs.renameSync(filePath, reservedPath);
311
+ } catch (error) {
312
+ if (error.code === "ENOENT") {
313
+ throw new ConsentReceiptError(
314
+ "missing",
315
+ `Consent receipt "${receipt.id}" is already reserved or consumed.`
316
+ );
317
+ }
318
+ throw error;
319
+ }
320
+ writeReservedReceiptFile(reservedPath, receipt, reservationOwnerId, now);
321
+ return reservedPath;
322
+ }
323
+ function mintPairToken() {
324
+ return randomBytes(32).toString("base64url");
325
+ }
326
+ function writeSecretFile(secretPath, pairToken, mintedAt) {
327
+ fs.writeFileSync(secretPath, pairToken, {
328
+ encoding: "utf-8",
329
+ mode: 384
330
+ });
331
+ stampMintedAt(secretPath, mintedAt);
332
+ hardenSecretStorePath(secretPath, 384);
333
+ }
334
+ function assertNoLegacyPairTokenInput(options, context) {
335
+ const legacyPairToken = options.pairToken;
336
+ if (typeof legacyPairToken !== "undefined") {
337
+ throw new ConsentReceiptError(
338
+ "invalid",
339
+ `${context} no longer accepts a caller-provided pairToken.`
340
+ );
341
+ }
342
+ }
343
+ function createConsentReceipt(options) {
344
+ assertNoLegacyPairTokenInput(options, "createConsentReceipt");
345
+ const now = options.now ?? /* @__PURE__ */ new Date();
346
+ const { receiptsDir, secretsDir } = resolveConsentDirs(options);
347
+ const scope = options.scope ?? "drive";
348
+ const conversationId = options.conversationId.trim();
349
+ if (!conversationId) {
350
+ throw new ConsentReceiptError(
351
+ "invalid",
352
+ "Consent receipt requires a non-empty conversationId."
353
+ );
354
+ }
355
+ fs.mkdirSync(receiptsDir, { recursive: true });
356
+ fs.mkdirSync(secretsDir, { recursive: true, mode: 448 });
357
+ hardenSecretStorePath(secretsDir, 448);
358
+ cleanupExpiredReceipts(receiptsDir, secretsDir, now);
359
+ const ttlSeconds = Math.max(
360
+ 1,
361
+ options.ttlSeconds ?? DEFAULT_CONSENT_TTL_SECONDS
362
+ );
363
+ const receiptId = randomUUID();
364
+ const hostId = normalizeString(options.hostId);
365
+ const ownerClientId = normalizeString(options.ownerClientId);
366
+ const pairToken = mintPairToken();
367
+ const receipt = {
368
+ id: receiptId,
369
+ scope,
370
+ hostId,
371
+ conversationId,
372
+ ownerClientId,
373
+ issuedByClientId: normalizeString(options.issuedByClientId),
374
+ allowedMethods: normalizeMethods(options.allowedMethods),
375
+ pairTokenHash: hashPairTokenBinding({
376
+ pairToken,
377
+ hostId,
378
+ conversationId,
379
+ ownerClientId
380
+ }),
381
+ createdAt: now.toISOString(),
382
+ expiresAt: new Date(now.getTime() + ttlSeconds * 1e3).toISOString()
383
+ };
384
+ const filePath = path.join(receiptsDir, `${receipt.id}.json`);
385
+ const secretPath = resolveSecretPath(secretsDir, receipt.id);
386
+ const createdAt = new Date(receipt.createdAt);
387
+ try {
388
+ writeSecretFile(secretPath, pairToken, createdAt);
389
+ fs.writeFileSync(filePath, JSON.stringify(receipt, null, 2), "utf-8");
390
+ stampMintedAt(filePath, createdAt);
391
+ } catch (error) {
392
+ removeSecretPath(secretPath);
393
+ removeReceiptPath(filePath);
394
+ throw error;
395
+ }
396
+ return { receipt, filePath };
397
+ }
398
+ function prepareConsentReceipt(options) {
399
+ assertNoLegacyPairTokenInput(options, "consumeConsentReceipt");
400
+ const now = options.now ?? /* @__PURE__ */ new Date();
401
+ const { receiptsDir, secretsDir } = resolveConsentDirs(options);
402
+ cleanupExpiredReceipts(receiptsDir, secretsDir, now);
403
+ const requiredScope = options.requiredScope ?? "drive";
404
+ const method = normalizeString(options.method);
405
+ const conversationId = options.conversationId.trim();
406
+ const ownerClientId = normalizeString(options.ownerClientId);
407
+ const hostId = normalizeString(options.hostId);
408
+ const reservationOwnerId = normalizeString(options.reservationOwnerId);
409
+ const explicitConsentRef = normalizeString(options.consentRef);
410
+ if (!conversationId) {
411
+ throw new ConsentReceiptError(
412
+ "invalid",
413
+ "Consent receipt consumption requires a conversationId."
414
+ );
415
+ }
416
+ const explicitPath = resolveReceiptPath(receiptsDir, explicitConsentRef);
417
+ const explicitReservedPath = explicitConsentRef ? resolveReservedReceiptPath(receiptsDir, explicitConsentRef) : null;
418
+ const reservedConsentRef = explicitConsentRef;
419
+ if (reservedConsentRef && explicitPath && explicitReservedPath && !fs.existsSync(explicitPath) && fs.existsSync(explicitReservedPath)) {
420
+ assertPendingReservationAvailable(reservedConsentRef);
421
+ const reservedRecord = loadReservedReceiptRecord(explicitReservedPath);
422
+ const reservedReceipt = reservedRecord.receipt;
423
+ const reservedReceiptId = reservedReceipt?.id ?? extractReceiptIdFromPath(explicitReservedPath);
424
+ if (!reservedReceipt || isExpired(reservedReceipt, now)) {
425
+ removeReceiptPath(explicitReservedPath);
426
+ removeSecretPath(resolveSecretPath(secretsDir, reservedReceiptId));
427
+ } else if (reservationOwnerId && reservedRecord.reservationOwnerId === reservationOwnerId && isReservationExpired(fs.statSync(explicitReservedPath), now)) {
428
+ fs.renameSync(explicitReservedPath, explicitPath);
429
+ writeActiveReceiptFile(explicitPath, reservedReceipt);
430
+ } else {
431
+ throw new ConsentReceiptError(
432
+ "missing",
433
+ `Consent receipt "${explicitConsentRef}" is already reserved or consumed.`
434
+ );
435
+ }
436
+ }
437
+ const candidatePaths = explicitPath ? [explicitPath] : listReceiptPaths(receiptsDir);
438
+ let deferredError = null;
439
+ for (const filePath of candidatePaths) {
440
+ if (!fs.existsSync(filePath)) {
441
+ if (explicitPath && explicitReservedPath && fs.existsSync(explicitReservedPath)) {
442
+ throw new ConsentReceiptError(
443
+ "missing",
444
+ `Consent receipt "${explicitConsentRef}" is already reserved or consumed.`
445
+ );
446
+ }
447
+ continue;
448
+ }
449
+ const receiptStats = fs.statSync(filePath);
450
+ const receipt = loadConsentReceipt(filePath);
451
+ if (!receipt) {
452
+ removeReceiptPath(filePath);
453
+ removeSecretPath(
454
+ resolveSecretPath(secretsDir, extractReceiptIdFromPath(filePath))
455
+ );
456
+ continue;
457
+ }
458
+ if (isExpired(receipt, now)) {
459
+ removeReceiptPath(filePath);
460
+ removeSecretPath(resolveSecretPath(secretsDir, receipt.id));
461
+ if (explicitPath) {
462
+ throw new ConsentReceiptError(
463
+ "expired",
464
+ `Consent receipt "${receipt.id}" expired at ${receipt.expiresAt}.`
465
+ );
466
+ }
467
+ continue;
468
+ }
469
+ const secretPath = resolveSecretPath(secretsDir, receipt.id);
470
+ if (!fs.existsSync(secretPath)) {
471
+ if (explicitPath) {
472
+ throw new ConsentReceiptError(
473
+ "missing",
474
+ `Consent secret "${receipt.id}" was not found.`
475
+ );
476
+ }
477
+ continue;
478
+ }
479
+ let receiptPrepared = false;
480
+ let cleanupSecretOnFailure = true;
481
+ try {
482
+ assertUntamperedConsentPath(receiptStats, receipt, "receipt");
483
+ const secretStats = fs.statSync(secretPath);
484
+ assertUntamperedConsentPath(secretStats, receipt, "secret");
485
+ const pairToken = readUtf8PreservingTimes(secretPath).trim();
486
+ if (!pairToken) {
487
+ throw new ConsentReceiptError(
488
+ "invalid",
489
+ `Consent secret "${receipt.id}" was empty.`
490
+ );
491
+ }
492
+ const expectedHash = hashPairTokenBinding({
493
+ pairToken,
494
+ hostId,
495
+ conversationId,
496
+ ownerClientId
497
+ });
498
+ if (receipt.conversationId !== conversationId || receipt.ownerClientId !== ownerClientId || receipt.hostId !== hostId || receipt.pairTokenHash !== expectedHash) {
499
+ if (explicitPath) {
500
+ throw new ConsentReceiptError(
501
+ "binding-mismatch",
502
+ `Consent receipt "${receipt.id}" did not match the requested conversation binding.`
503
+ );
504
+ }
505
+ continue;
506
+ }
507
+ if (!scopeSatisfies(receipt.scope, requiredScope)) {
508
+ deferredError = new ConsentReceiptError(
509
+ "scope-mismatch",
510
+ `Consent receipt "${receipt.id}" grants ${receipt.scope}, not ${requiredScope}.`
511
+ );
512
+ if (explicitPath) throw deferredError;
513
+ continue;
514
+ }
515
+ if (method && receipt.allowedMethods.length > 0 && !receipt.allowedMethods.includes(method)) {
516
+ deferredError = new ConsentReceiptError(
517
+ "method-mismatch",
518
+ `Consent receipt "${receipt.id}" does not allow method "${method}".`
519
+ );
520
+ if (explicitPath) throw deferredError;
521
+ continue;
522
+ }
523
+ let reservedReceiptPath;
524
+ try {
525
+ assertPendingReservationAvailable(receipt.id);
526
+ reservedReceiptPath = reserveReceiptPath(
527
+ filePath,
528
+ receipt,
529
+ reservationOwnerId,
530
+ now
531
+ );
532
+ } catch (error) {
533
+ cleanupSecretOnFailure = false;
534
+ throw error;
535
+ }
536
+ markPendingReservation(receipt.id);
537
+ receiptPrepared = true;
538
+ return {
539
+ receipt,
540
+ commit() {
541
+ if (!receiptPrepared) {
542
+ return;
543
+ }
544
+ receiptPrepared = false;
545
+ try {
546
+ fs.rmSync(reservedReceiptPath, { force: false });
547
+ } finally {
548
+ clearPendingReservation(receipt.id);
549
+ removeSecretPath(secretPath);
550
+ }
551
+ },
552
+ abort() {
553
+ if (!receiptPrepared) {
554
+ return;
555
+ }
556
+ receiptPrepared = false;
557
+ try {
558
+ fs.renameSync(reservedReceiptPath, filePath);
559
+ writeActiveReceiptFile(filePath, receipt);
560
+ } finally {
561
+ clearPendingReservation(receipt.id);
562
+ }
563
+ }
564
+ };
565
+ } finally {
566
+ if (!receiptPrepared && cleanupSecretOnFailure) {
567
+ removeSecretPath(secretPath);
568
+ }
569
+ }
570
+ }
571
+ if (deferredError) {
572
+ throw deferredError;
573
+ }
574
+ throw new ConsentReceiptError(
575
+ "missing",
576
+ explicitPath ? `Consent receipt "${options.consentRef}" was not found.` : "No matching consent receipt was found for the requested drive action."
577
+ );
578
+ }
579
+
580
+ // src/transport/consent-ledger.ts
581
+ import { randomUUID as randomUUID2 } from "crypto";
582
+ import * as fs2 from "fs";
583
+ import * as path2 from "path";
584
+ function normalizeString2(value) {
585
+ const normalized = value?.trim();
586
+ return normalized ? normalized : null;
587
+ }
588
+ function normalizeAddress(value) {
589
+ if (!value) {
590
+ return null;
591
+ }
592
+ const address = {
593
+ hostId: normalizeString2(value.hostId),
594
+ clientId: normalizeString2(value.clientId),
595
+ conversationId: normalizeString2(value.conversationId),
596
+ ownerClientId: normalizeString2(value.ownerClientId)
597
+ };
598
+ return Object.values(address).some((field) => field) ? address : null;
599
+ }
600
+ function isConsentLedgerEnabled() {
601
+ const normalized = process.env.TAP_CONSENT_LEDGER?.trim().toLowerCase();
602
+ if (!normalized) {
603
+ return true;
604
+ }
605
+ return !["0", "false", "no", "off"].includes(normalized);
606
+ }
607
+ function resolveConsentLedgerDir(commsDir) {
608
+ const resolvedCommsDir = normalizeString2(commsDir) ?? normalizeString2(process.env.TAP_COMMS_DIR);
609
+ if (!resolvedCommsDir) {
610
+ return null;
611
+ }
612
+ return path2.join(
613
+ path2.resolve(resolvedCommsDir),
614
+ "receipts",
615
+ "consent-ledger"
616
+ );
617
+ }
618
+ var MISSING_CONSENT_REF_ORPHAN_REASON = "missing_consent_ref";
619
+ function resolveGrantId(event, grantId) {
620
+ if (grantId) {
621
+ return {
622
+ grantId,
623
+ orphanReason: null
624
+ };
625
+ }
626
+ if (event !== "rejected") {
627
+ return {
628
+ grantId: null,
629
+ orphanReason: null
630
+ };
631
+ }
632
+ return {
633
+ grantId: `orphan-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`,
634
+ orphanReason: MISSING_CONSENT_REF_ORPHAN_REASON
635
+ };
636
+ }
637
+ function formatLedgerTimestamp(value) {
638
+ return value.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
639
+ }
640
+ function buildLedgerFilePath(ledgerDir, record) {
641
+ const timestamp = formatLedgerTimestamp(record.recordedAt);
642
+ const shortGrantId = record.grantId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "unknown";
643
+ const baseName = `${timestamp}-${record.event}-${shortGrantId}`;
644
+ const preferredPath = path2.join(ledgerDir, `${baseName}.md`);
645
+ if (!fs2.existsSync(preferredPath)) {
646
+ return preferredPath;
647
+ }
648
+ return path2.join(
649
+ ledgerDir,
650
+ `${baseName}-${randomUUID2().replace(/-/g, "").slice(0, 6)}.md`
651
+ );
652
+ }
653
+ function buildFrontmatter(record) {
654
+ const fields = [
655
+ ["type", "consent-ledger"],
656
+ ["event", record.event],
657
+ ["grant_id", record.grantId],
658
+ ["orphan_reason", record.orphanReason],
659
+ ["scope", record.scope],
660
+ ["method", record.method],
661
+ ["host_id", record.hostId],
662
+ ["conversation_id", record.conversationId],
663
+ ["issued_at", record.issuedAt],
664
+ ["expires_at", record.expiresAt],
665
+ ["consumed_at", record.consumedAt],
666
+ ["recorded_at", record.recordedAt],
667
+ ["result", record.result],
668
+ ["issued_by_client_id", record.issuedByClientId],
669
+ ["requester", record.requester],
670
+ ["owner", record.owner]
671
+ ];
672
+ const lines = fields.map(
673
+ ([key, value]) => `${key}: ${JSON.stringify(value ?? null)}`
674
+ );
675
+ return `---
676
+ ${lines.join("\n")}
677
+ ---
678
+
679
+ `;
680
+ }
681
+ function buildBody(record) {
682
+ return [
683
+ "# Consent Ledger Event",
684
+ "",
685
+ `- Event: \`${record.event}\``,
686
+ `- Grant: \`${record.grantId}\``,
687
+ ...record.orphanReason ? [`- Orphan Reason: \`${record.orphanReason}\``] : [],
688
+ `- Scope: \`${record.scope}\``,
689
+ `- Result: \`${record.result}\``,
690
+ "",
691
+ "## Owner",
692
+ "",
693
+ "```json",
694
+ JSON.stringify(record.owner, null, 2),
695
+ "```",
696
+ "",
697
+ "## Requester",
698
+ "",
699
+ "```json",
700
+ JSON.stringify(record.requester, null, 2),
701
+ "```",
702
+ ""
703
+ ].join("\n");
704
+ }
705
+ function writeConsentLedgerEvent(options) {
706
+ if (!isConsentLedgerEnabled()) {
707
+ return null;
708
+ }
709
+ const { grantId, orphanReason } = resolveGrantId(
710
+ options.event,
711
+ normalizeString2(options.grantId)
712
+ );
713
+ const result = normalizeString2(options.result);
714
+ const ledgerDir = resolveConsentLedgerDir(options.commsDir);
715
+ if (!grantId || !result || !ledgerDir) {
716
+ return null;
717
+ }
718
+ const record = {
719
+ event: options.event,
720
+ grantId,
721
+ orphanReason,
722
+ scope: options.scope,
723
+ method: normalizeString2(options.method),
724
+ hostId: normalizeString2(options.hostId),
725
+ conversationId: normalizeString2(options.conversationId),
726
+ issuedAt: normalizeString2(options.issuedAt),
727
+ expiresAt: normalizeString2(options.expiresAt),
728
+ consumedAt: normalizeString2(options.consumedAt),
729
+ recordedAt: normalizeString2(options.recordedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
730
+ result,
731
+ requester: normalizeAddress(options.requester),
732
+ owner: normalizeAddress(options.owner),
733
+ issuedByClientId: normalizeString2(options.issuedByClientId)
734
+ };
735
+ try {
736
+ fs2.mkdirSync(ledgerDir, { recursive: true });
737
+ const filePath = buildLedgerFilePath(ledgerDir, record);
738
+ fs2.writeFileSync(
739
+ filePath,
740
+ buildFrontmatter(record) + buildBody(record),
741
+ "utf-8"
742
+ );
743
+ return filePath;
744
+ } catch {
745
+ return null;
746
+ }
747
+ }
748
+
749
+ // src/transport/experimental/codex-ipc-observe.ts
750
+ import * as net from "net";
751
+ import { randomUUID as randomUUID3 } from "crypto";
752
+
753
+ // src/transport/experimental/codex-ipc-endpoint.ts
754
+ import { tmpdir as tmpdir2 } from "os";
755
+ var DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH = String.raw`\\.\pipe\codex-ipc`;
756
+ function normalizeDirectory(value) {
757
+ return value.replace(/[\\/]+$/, "");
758
+ }
759
+ function resolveCodexIpcPath(options = {}) {
760
+ const env = options.env ?? process.env;
761
+ const explicit = env.TAP_CODEX_IPC_PATH?.trim();
762
+ if (explicit) return explicit;
763
+ const platform = options.platform ?? process.platform;
764
+ if (platform === "win32") return DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH;
765
+ if (platform === "darwin") {
766
+ const baseTmp = normalizeDirectory(
767
+ options.tmpDir?.trim() || env.TMPDIR?.trim() || tmpdir2()
768
+ );
769
+ const uid = typeof options.uid === "number" && Number.isFinite(options.uid) ? options.uid : typeof process.getuid === "function" ? process.getuid() : null;
770
+ if (uid == null) {
771
+ throw new Error("Cannot resolve macOS Codex IPC socket without a uid.");
772
+ }
773
+ return `${baseTmp}/codex-ipc/ipc-${uid}.sock`;
774
+ }
775
+ return DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH;
776
+ }
777
+
778
+ // src/transport/experimental/codex-ipc-observe.ts
779
+ var MAX_FRAME_BYTES = 256 * 1024 * 1024;
780
+ var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
781
+ var DEFAULT_TARGETED_REQUEST_VERSION = 1;
782
+ function isTapIpcTraceEnabled() {
783
+ const value = process.env.TAP_IPC_TRACE?.trim().toLowerCase();
784
+ return value === "1" || value === "true" || value === "yes" || value === "on";
785
+ }
786
+ function formatTraceValue(value) {
787
+ if (typeof value === "string") {
788
+ return JSON.stringify(value);
789
+ }
790
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
791
+ return String(value);
792
+ }
793
+ if (value === null) {
794
+ return "null";
795
+ }
796
+ return JSON.stringify(value);
797
+ }
798
+ function formatTraceContext(context) {
799
+ if (!context) return "";
800
+ const entries = Object.entries(context).filter(
801
+ ([, value]) => typeof value !== "undefined"
802
+ );
803
+ if (entries.length === 0) return "";
804
+ return ` ${entries.map(([key, value]) => `${key}=${formatTraceValue(value)}`).join(" ")}`;
805
+ }
806
+ function resolveHostId(explicitHostId) {
807
+ const normalizedExplicit = explicitHostId?.trim();
808
+ if (normalizedExplicit) return normalizedExplicit;
809
+ const computerName = process.env.COMPUTERNAME?.trim();
810
+ if (computerName) return computerName;
811
+ const hostName = process.env.HOSTNAME?.trim();
812
+ if (hostName) return hostName;
813
+ return null;
814
+ }
815
+ function asRecord(value) {
816
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
817
+ return value;
818
+ }
819
+ function asString(value) {
820
+ return typeof value === "string" && value.trim() ? value.trim() : null;
821
+ }
822
+ function getStringField(record, ...keys) {
823
+ if (!record) return null;
824
+ for (const key of keys) {
825
+ const value = asString(record[key]);
826
+ if (value) return value;
827
+ }
828
+ return null;
829
+ }
830
+ function normalizeTransportAddress(hostId, clientId, conversationId, ownerClientId) {
831
+ return {
832
+ hostId,
833
+ clientId,
834
+ conversationId,
835
+ ownerClientId
836
+ };
837
+ }
838
+ function extractConversationId(params) {
839
+ return getStringField(params, "conversationId", "threadId") ?? getStringField(asRecord(params?.change), "conversationId", "threadId") ?? getStringField(asRecord(params?.thread), "id");
840
+ }
841
+ function listRecordKeys(value) {
842
+ if (!value) return null;
843
+ return Object.keys(value);
844
+ }
845
+ function encodeCodexIpcFrame(message) {
846
+ const json = JSON.stringify(message);
847
+ const payload = Buffer.from(json, "utf-8");
848
+ const frame = Buffer.allocUnsafe(4 + payload.length);
849
+ frame.writeUInt32LE(payload.length, 0);
850
+ payload.copy(frame, 4);
851
+ return frame;
852
+ }
853
+ function decodeCodexIpcFrames(buffer) {
854
+ const messages = [];
855
+ let offset = 0;
856
+ while (offset + 4 <= buffer.length) {
857
+ const frameLength = buffer.readUInt32LE(offset);
858
+ if (frameLength > MAX_FRAME_BYTES) {
859
+ throw new Error(
860
+ `Codex IPC frame exceeds max size (${frameLength} bytes > ${MAX_FRAME_BYTES})`
861
+ );
862
+ }
863
+ if (offset + 4 + frameLength > buffer.length) break;
864
+ const json = buffer.toString("utf-8", offset + 4, offset + 4 + frameLength);
865
+ messages.push(JSON.parse(json));
866
+ offset += 4 + frameLength;
867
+ }
868
+ return {
869
+ messages,
870
+ remainder: buffer.subarray(offset)
871
+ };
872
+ }
873
+ var ExperimentalCodexIpcObserveTransport = class {
874
+ constructor(options = {}) {
875
+ this.options = options;
876
+ this.pipePath = options.pipePath ?? resolveCodexIpcPath();
877
+ this.hostId = resolveHostId(options.hostId);
878
+ this.clientType = options.clientType ?? "tap-observe";
879
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
880
+ }
881
+ options;
882
+ kind = "experimental-codex-ipc-observe";
883
+ pipePath;
884
+ hostId;
885
+ clientType;
886
+ requestTimeoutMs;
887
+ listeners = /* @__PURE__ */ new Set();
888
+ agents = /* @__PURE__ */ new Map();
889
+ conversations = /* @__PURE__ */ new Map();
890
+ pendingRequests = /* @__PURE__ */ new Map();
891
+ socket = null;
892
+ remainder = Buffer.alloc(0);
893
+ connectedAt = null;
894
+ ownClientId = null;
895
+ snapshot = {
896
+ transport: this.kind,
897
+ connected: false,
898
+ connectedAt: null,
899
+ agents: [],
900
+ conversations: []
901
+ };
902
+ handleData = (...args) => {
903
+ const [chunk] = args;
904
+ if (!Buffer.isBuffer(chunk)) {
905
+ return;
906
+ }
907
+ this.remainder = Buffer.concat([this.remainder, chunk]);
908
+ const decoded = decodeCodexIpcFrames(this.remainder);
909
+ this.remainder = decoded.remainder;
910
+ for (const message of decoded.messages) {
911
+ this.handleMessage(message);
912
+ }
913
+ };
914
+ handleError = (...args) => {
915
+ const [error] = args;
916
+ this.rejectPendingRequests(
917
+ error instanceof Error ? error : new Error(String(error ?? "Codex IPC transport error"))
918
+ );
919
+ };
920
+ handleClose = () => {
921
+ this.rejectPendingRequests(new Error("Codex IPC transport closed"));
922
+ this.remainder = Buffer.alloc(0);
923
+ this.emitDisconnected(null);
924
+ this.detachSocket();
925
+ };
926
+ async connect() {
927
+ if (this.socket) {
928
+ await this.disconnect();
929
+ }
930
+ this.trace("connect:start", {
931
+ pipePath: this.pipePath,
932
+ clientType: this.clientType,
933
+ hostId: this.hostId
934
+ });
935
+ const socket = this.options.socketFactory?.(this.pipePath) ?? net.createConnection({
936
+ path: this.pipePath
937
+ });
938
+ this.socket = socket;
939
+ this.attachSocket(socket);
940
+ await this.waitForConnect(socket);
941
+ socket.setNoDelay?.(true);
942
+ this.trace("connect:open", {
943
+ pipePath: this.pipePath
944
+ });
945
+ const response = await this.sendRequest("initialize", {
946
+ clientType: this.clientType
947
+ });
948
+ const result = asRecord(response.result);
949
+ const clientId = getStringField(result, "clientId");
950
+ if (!clientId) {
951
+ throw new Error("Codex IPC initialize response did not include clientId");
952
+ }
953
+ this.ownClientId = clientId;
954
+ this.connectedAt = (/* @__PURE__ */ new Date()).toISOString();
955
+ this.snapshot = this.buildSnapshot(true);
956
+ this.trace("connect:initialized", {
957
+ clientId,
958
+ connectedAt: this.connectedAt,
959
+ handledByClientId: response.handledByClientId ?? null,
960
+ resultType: response.resultType ?? null,
961
+ resultKeys: listRecordKeys(result)
962
+ });
963
+ this.emit({
964
+ kind: "transport-connected",
965
+ receivedAt: this.connectedAt,
966
+ method: "initialize",
967
+ sourceAddress: normalizeTransportAddress(
968
+ this.hostId,
969
+ this.ownClientId,
970
+ null,
971
+ null
972
+ ),
973
+ payload: response,
974
+ snapshot: this.snapshot
975
+ });
976
+ return this.snapshot;
977
+ }
978
+ async disconnect() {
979
+ if (!this.socket) return;
980
+ const socket = this.socket;
981
+ this.detachSocket();
982
+ this.rejectPendingRequests(new Error("Codex IPC transport disconnected"));
983
+ this.remainder = Buffer.alloc(0);
984
+ this.emitDisconnected({ reason: "disconnect" });
985
+ socket.end();
986
+ socket.destroy();
987
+ }
988
+ getSnapshot() {
989
+ return this.snapshot;
990
+ }
991
+ subscribe(listener) {
992
+ this.listeners.add(listener);
993
+ return () => {
994
+ this.listeners.delete(listener);
995
+ };
996
+ }
997
+ attachSocket(socket) {
998
+ socket.on("data", this.handleData);
999
+ socket.on("error", this.handleError);
1000
+ socket.on("close", this.handleClose);
1001
+ }
1002
+ emitDisconnected(payload) {
1003
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
1004
+ this.connectedAt = null;
1005
+ this.snapshot = this.buildSnapshot(false);
1006
+ this.emit({
1007
+ kind: "transport-disconnected",
1008
+ receivedAt,
1009
+ method: null,
1010
+ sourceAddress: normalizeTransportAddress(
1011
+ this.hostId,
1012
+ this.ownClientId,
1013
+ null,
1014
+ null
1015
+ ),
1016
+ payload,
1017
+ snapshot: this.snapshot
1018
+ });
1019
+ }
1020
+ detachSocket() {
1021
+ if (!this.socket) return;
1022
+ this.socket.removeListener("data", this.handleData);
1023
+ this.socket.removeListener("error", this.handleError);
1024
+ this.socket.removeListener("close", this.handleClose);
1025
+ this.socket = null;
1026
+ }
1027
+ async waitForConnect(socket) {
1028
+ await new Promise((resolve3, reject) => {
1029
+ const cleanup = () => {
1030
+ clearTimeout(timeout);
1031
+ socket.removeListener("connect", onConnect);
1032
+ socket.removeListener("error", onError);
1033
+ };
1034
+ const onConnect = () => {
1035
+ cleanup();
1036
+ resolve3();
1037
+ };
1038
+ const onError = (...args) => {
1039
+ const [error] = args;
1040
+ cleanup();
1041
+ reject(
1042
+ error instanceof Error ? error : new Error(String(error ?? "Codex IPC connection failed"))
1043
+ );
1044
+ };
1045
+ const timeout = setTimeout(() => {
1046
+ cleanup();
1047
+ reject(
1048
+ new Error(
1049
+ `Timed out connecting to Codex IPC transport at ${this.pipePath}`
1050
+ )
1051
+ );
1052
+ }, this.requestTimeoutMs);
1053
+ socket.on("connect", onConnect);
1054
+ socket.on("error", onError);
1055
+ });
1056
+ }
1057
+ getHostId() {
1058
+ return this.hostId;
1059
+ }
1060
+ getOwnClientId() {
1061
+ return this.ownClientId;
1062
+ }
1063
+ trace(message, context) {
1064
+ if (!isTapIpcTraceEnabled()) {
1065
+ return;
1066
+ }
1067
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
1068
+ console.log(
1069
+ `[${timestamp}] TAP_IPC_TRACE [${this.kind}] ${message}${formatTraceContext(context)}`
1070
+ );
1071
+ }
1072
+ resolveRequestVersion(_method, targetClientId) {
1073
+ if (this.options.protocolVersion !== null) {
1074
+ const configuredVersion = this.options.protocolVersion;
1075
+ if (typeof configuredVersion !== "undefined") {
1076
+ return configuredVersion;
1077
+ }
1078
+ }
1079
+ if (targetClientId?.trim()) {
1080
+ return DEFAULT_TARGETED_REQUEST_VERSION;
1081
+ }
1082
+ return null;
1083
+ }
1084
+ async sendRequest(method, params, targetClientId) {
1085
+ if (!this.socket) {
1086
+ throw new Error("Codex IPC observe transport is not connected");
1087
+ }
1088
+ const requestId = randomUUID3();
1089
+ const message = {
1090
+ type: "request",
1091
+ requestId,
1092
+ method,
1093
+ params
1094
+ };
1095
+ if (this.ownClientId) {
1096
+ message.sourceClientId = this.ownClientId;
1097
+ }
1098
+ const requestVersion = this.resolveRequestVersion(method, targetClientId);
1099
+ if (requestVersion !== null) {
1100
+ message.version = requestVersion;
1101
+ }
1102
+ if (targetClientId) {
1103
+ message.targetClientId = targetClientId;
1104
+ }
1105
+ this.trace("request:send", {
1106
+ requestId,
1107
+ method,
1108
+ targetClientId: targetClientId ?? null,
1109
+ version: message.version ?? null,
1110
+ conversationId: extractConversationId(params ?? null),
1111
+ paramKeys: listRecordKeys(params ?? null)
1112
+ });
1113
+ const promise = new Promise((resolve3, reject) => {
1114
+ const timeout = setTimeout(() => {
1115
+ this.pendingRequests.delete(requestId);
1116
+ reject(
1117
+ new Error(
1118
+ `Codex IPC request "${method}" timed out after ${this.requestTimeoutMs}ms`
1119
+ )
1120
+ );
1121
+ }, this.requestTimeoutMs);
1122
+ this.pendingRequests.set(requestId, { resolve: resolve3, reject, timeout });
1123
+ });
1124
+ this.socket.write(encodeCodexIpcFrame(message));
1125
+ return promise;
1126
+ }
1127
+ handleMessage(message) {
1128
+ if (message.type === "response") {
1129
+ this.handleResponse(message);
1130
+ return;
1131
+ }
1132
+ if (message.type === "broadcast") {
1133
+ this.handleBroadcast(message);
1134
+ }
1135
+ }
1136
+ handleResponse(message) {
1137
+ const requestId = asString(message.requestId);
1138
+ if (!requestId) return;
1139
+ const pending = this.pendingRequests.get(requestId);
1140
+ if (!pending) return;
1141
+ clearTimeout(pending.timeout);
1142
+ this.pendingRequests.delete(requestId);
1143
+ this.trace("response:recv", {
1144
+ requestId,
1145
+ method: message.method ?? null,
1146
+ resultType: message.resultType ?? null,
1147
+ handledByClientId: message.handledByClientId ?? null,
1148
+ hasError: message.error != null,
1149
+ hasResult: typeof message.result !== "undefined"
1150
+ });
1151
+ if (message.resultType === "error") {
1152
+ pending.reject(
1153
+ new Error(
1154
+ `Codex IPC request failed: ${JSON.stringify(message.error ?? {})}`
1155
+ )
1156
+ );
1157
+ return;
1158
+ }
1159
+ pending.resolve(message);
1160
+ }
1161
+ handleBroadcast(message) {
1162
+ const method = message.method ?? null;
1163
+ const params = asRecord(message.params);
1164
+ const sourceClientId = asString(message.sourceClientId);
1165
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
1166
+ this.trace("broadcast:recv", {
1167
+ method,
1168
+ sourceClientId,
1169
+ conversationId: extractConversationId(params),
1170
+ version: message.version ?? null
1171
+ });
1172
+ if (method === "client-status-changed") {
1173
+ const clientId = getStringField(params, "clientId");
1174
+ if (clientId) {
1175
+ this.upsertAgent(clientId, {
1176
+ name: getStringField(params, "clientType"),
1177
+ metadata: {
1178
+ status: getStringField(params, "status"),
1179
+ clientType: getStringField(params, "clientType")
1180
+ }
1181
+ });
1182
+ this.snapshot = this.buildSnapshot(true);
1183
+ this.emit({
1184
+ kind: "agent-status",
1185
+ receivedAt,
1186
+ method,
1187
+ sourceAddress: normalizeTransportAddress(
1188
+ this.hostId,
1189
+ clientId,
1190
+ null,
1191
+ null
1192
+ ),
1193
+ payload: message,
1194
+ snapshot: this.snapshot
1195
+ });
1196
+ }
1197
+ return;
1198
+ }
1199
+ if (method === "thread-stream-state-changed") {
1200
+ const conversationId = extractConversationId(params);
1201
+ if (conversationId) {
1202
+ const ownerClientId = sourceClientId;
1203
+ if (ownerClientId) {
1204
+ this.upsertAgent(ownerClientId, {
1205
+ name: null,
1206
+ metadata: {}
1207
+ });
1208
+ }
1209
+ this.conversations.set(conversationId, {
1210
+ id: conversationId,
1211
+ address: normalizeTransportAddress(
1212
+ this.hostId,
1213
+ ownerClientId,
1214
+ conversationId,
1215
+ ownerClientId
1216
+ ),
1217
+ metadata: {
1218
+ change: params?.change ?? null,
1219
+ lastMethod: method,
1220
+ sourceClientId: ownerClientId
1221
+ }
1222
+ });
1223
+ this.snapshot = this.buildSnapshot(true);
1224
+ this.emit({
1225
+ kind: "conversation-state",
1226
+ receivedAt,
1227
+ method,
1228
+ sourceAddress: normalizeTransportAddress(
1229
+ this.hostId,
1230
+ ownerClientId,
1231
+ conversationId,
1232
+ ownerClientId
1233
+ ),
1234
+ payload: message,
1235
+ snapshot: this.snapshot
1236
+ });
1237
+ return;
1238
+ }
1239
+ }
1240
+ this.snapshot = this.buildSnapshot(true);
1241
+ this.emit({
1242
+ kind: "raw",
1243
+ receivedAt,
1244
+ method,
1245
+ sourceAddress: normalizeTransportAddress(
1246
+ this.hostId,
1247
+ sourceClientId,
1248
+ extractConversationId(params),
1249
+ sourceClientId
1250
+ ),
1251
+ payload: message,
1252
+ snapshot: this.snapshot
1253
+ });
1254
+ }
1255
+ upsertAgent(clientId, update) {
1256
+ const existing = this.agents.get(clientId);
1257
+ this.agents.set(clientId, {
1258
+ id: clientId,
1259
+ name: update.name ?? existing?.name ?? null,
1260
+ address: normalizeTransportAddress(this.hostId, clientId, null, null),
1261
+ metadata: {
1262
+ ...existing?.metadata ?? {},
1263
+ ...update.metadata
1264
+ }
1265
+ });
1266
+ }
1267
+ buildSnapshot(connected) {
1268
+ return {
1269
+ transport: this.kind,
1270
+ connected,
1271
+ connectedAt: connected ? this.connectedAt : null,
1272
+ agents: [...this.agents.values()].sort(
1273
+ (a, b) => a.id.localeCompare(b.id)
1274
+ ),
1275
+ conversations: [...this.conversations.values()].sort(
1276
+ (a, b) => a.id.localeCompare(b.id)
1277
+ )
1278
+ };
1279
+ }
1280
+ rejectPendingRequests(error) {
1281
+ for (const [requestId, pending] of this.pendingRequests) {
1282
+ clearTimeout(pending.timeout);
1283
+ pending.reject(error);
1284
+ this.pendingRequests.delete(requestId);
1285
+ }
1286
+ }
1287
+ emit(event) {
1288
+ for (const listener of this.listeners) {
1289
+ void listener(event);
1290
+ }
1291
+ }
1292
+ };
1293
+
1294
+ // src/transport/experimental/codex-ipc-control.ts
1295
+ function asJsonRecord(value) {
1296
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1297
+ return null;
1298
+ }
1299
+ return value;
1300
+ }
1301
+ var CODEX_IPC_DRIVE_METHODS = [
1302
+ "thread-follower-start-turn",
1303
+ "thread-follower-steer-turn",
1304
+ "thread-follower-interrupt-turn",
1305
+ "thread-follower-edit-last-user-turn",
1306
+ "thread-follower-submit-user-input",
1307
+ "thread-follower-submit-mcp-server-elicitation-response",
1308
+ "thread-follower-command-approval-decision",
1309
+ "thread-follower-file-approval-decision",
1310
+ "thread-follower-permissions-request-approval-response",
1311
+ "thread-follower-compact-thread",
1312
+ "thread-follower-set-model-and-reasoning",
1313
+ "thread-follower-set-collaboration-mode",
1314
+ "thread-follower-set-queued-follow-ups-state"
1315
+ ];
1316
+ var STABILITY_GUARDED_METHODS = /* @__PURE__ */ new Set([
1317
+ "thread-follower-start-turn"
1318
+ ]);
1319
+ var globalLocksKey = /* @__PURE__ */ Symbol.for("tap-comms:conversationLocks");
1320
+ var globalDriveTimeKey = /* @__PURE__ */ Symbol.for("tap-comms:conversationLastDriveTime");
1321
+ var globalStabilityGuardStore = globalThis;
1322
+ var sharedConversationLocks = globalStabilityGuardStore[globalLocksKey] ?? /* @__PURE__ */ new Map();
1323
+ if (!globalStabilityGuardStore[globalLocksKey]) {
1324
+ globalStabilityGuardStore[globalLocksKey] = sharedConversationLocks;
1325
+ }
1326
+ var sharedConversationLastDriveTime = globalStabilityGuardStore[globalDriveTimeKey] ?? /* @__PURE__ */ new Map();
1327
+ if (!globalStabilityGuardStore[globalDriveTimeKey]) {
1328
+ globalStabilityGuardStore[globalDriveTimeKey] = sharedConversationLastDriveTime;
1329
+ }
1330
+ function normalizeAddress2(value) {
1331
+ return {
1332
+ hostId: value.hostId?.trim() || null,
1333
+ clientId: value.clientId?.trim() || null,
1334
+ conversationId: value.conversationId?.trim() || null,
1335
+ ownerClientId: value.ownerClientId?.trim() || null
1336
+ };
1337
+ }
1338
+ function isDriveMethod(method) {
1339
+ return CODEX_IPC_DRIVE_METHODS.includes(method);
1340
+ }
1341
+ function normalizeMethod(method) {
1342
+ const normalized = method.trim();
1343
+ if (!isDriveMethod(normalized)) {
1344
+ throw new Error(`Unsupported Codex IPC drive method "${method}".`);
1345
+ }
1346
+ return normalized;
1347
+ }
1348
+ function normalizeActionLabel(action, method) {
1349
+ const normalized = action?.trim();
1350
+ return normalized || method;
1351
+ }
1352
+ function asRecord2(value) {
1353
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1354
+ return null;
1355
+ }
1356
+ return value;
1357
+ }
1358
+ function listRecordKeys2(value) {
1359
+ if (!value) {
1360
+ return null;
1361
+ }
1362
+ return Object.keys(value);
1363
+ }
1364
+ function summarizeDriveParams(params) {
1365
+ const turnStartParams = asRecord2(params?.turnStartParams);
1366
+ const input = Array.isArray(turnStartParams?.input) ? turnStartParams.input : null;
1367
+ const textLength = input?.reduce((total, item) => {
1368
+ const record = asRecord2(item);
1369
+ return total + (typeof record?.text === "string" ? record.text.length : 0);
1370
+ }, 0);
1371
+ return {
1372
+ paramKeys: listRecordKeys2(params),
1373
+ turnStartParamKeys: listRecordKeys2(turnStartParams),
1374
+ inputItemCount: input?.length ?? null,
1375
+ textLength: textLength ?? null
1376
+ };
1377
+ }
1378
+ function extractDriveTurnId(response) {
1379
+ const result = asRecord2(response.result);
1380
+ const nested = asRecord2(result?.result);
1381
+ const turn = asRecord2(result?.turn) ?? asRecord2(nested?.turn);
1382
+ const turnId = turn?.id;
1383
+ return typeof turnId === "string" && turnId.trim() ? turnId.trim() : null;
1384
+ }
1385
+ function extractConversationLastTurnStatus(conversation) {
1386
+ const change = asRecord2(conversation?.metadata.change);
1387
+ const turn = asRecord2(change?.turn);
1388
+ const turnStatus = turn?.status;
1389
+ if (typeof turnStatus === "string" && turnStatus.trim()) {
1390
+ return turnStatus.trim();
1391
+ }
1392
+ const conversationState = asRecord2(change?.conversationState);
1393
+ const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : null;
1394
+ const lastTurn = turns?.length ? asRecord2(turns[turns.length - 1]) : null;
1395
+ const lastStatus = lastTurn?.status;
1396
+ return typeof lastStatus === "string" && lastStatus.trim() ? lastStatus.trim() : null;
1397
+ }
1398
+ function extractRejectionResult(error) {
1399
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1400
+ return error.code;
1401
+ }
1402
+ return "execution-rejected";
1403
+ }
1404
+ function buildFollowerStartTurnParams(options) {
1405
+ const turnStartParams = { ...options.turnStartParams ?? {} };
1406
+ const text = options.text.trim();
1407
+ if (!text) {
1408
+ throw new Error(
1409
+ "thread-follower-start-turn requires a non-empty text input."
1410
+ );
1411
+ }
1412
+ const existingInput = Array.isArray(turnStartParams.input) ? turnStartParams.input : null;
1413
+ if (!existingInput) {
1414
+ turnStartParams.input = [
1415
+ {
1416
+ type: "text",
1417
+ text,
1418
+ text_elements: []
1419
+ }
1420
+ ];
1421
+ }
1422
+ if (!Array.isArray(turnStartParams.attachments)) {
1423
+ turnStartParams.attachments = [];
1424
+ }
1425
+ if (!Array.isArray(turnStartParams.commentAttachments)) {
1426
+ turnStartParams.commentAttachments = [];
1427
+ }
1428
+ if (typeof turnStartParams.inheritThreadSettings !== "boolean") {
1429
+ turnStartParams.inheritThreadSettings = true;
1430
+ }
1431
+ return {
1432
+ conversationId: options.conversationId,
1433
+ turnStartParams
1434
+ };
1435
+ }
1436
+ var ExperimentalCodexIpcControlTransport = class extends ExperimentalCodexIpcObserveTransport {
1437
+ kind = "experimental-codex-ipc-control";
1438
+ commsDir;
1439
+ receiptsDir;
1440
+ secretsDir;
1441
+ defaultConsentTtlSeconds;
1442
+ reservationOwnerId;
1443
+ conversationLocks = sharedConversationLocks;
1444
+ conversationLastDriveTime = sharedConversationLastDriveTime;
1445
+ COOLDOWN_MS = 1e4;
1446
+ LOCK_TIMEOUT_MS = 6e4;
1447
+ RECIPIENT_STATE_WAIT_MS = 750;
1448
+ constructor(options = {}) {
1449
+ super({
1450
+ ...options,
1451
+ clientType: options.clientType ?? "tap-control"
1452
+ });
1453
+ this.commsDir = options.commsDir;
1454
+ this.receiptsDir = options.receiptsDir;
1455
+ this.secretsDir = options.secretsDir;
1456
+ this.defaultConsentTtlSeconds = options.defaultConsentTtlSeconds ?? DEFAULT_CONSENT_TTL_SECONDS;
1457
+ this.reservationOwnerId = options.reservationOwnerId?.trim() || randomUUID4();
1458
+ this.subscribe((event) => {
1459
+ if (event.kind === "conversation-state") {
1460
+ const conversationId = event.sourceAddress.conversationId;
1461
+ if (!conversationId) return;
1462
+ const payload = asJsonRecord(event.payload);
1463
+ const params = asJsonRecord(payload?.params);
1464
+ const change = asJsonRecord(params?.change);
1465
+ const turn = asJsonRecord(change?.turn);
1466
+ if (turn) {
1467
+ const status = turn.status;
1468
+ this.trace("guard:observe-turn-status", {
1469
+ conversationId,
1470
+ turnId: turn.id,
1471
+ status
1472
+ });
1473
+ if (status === "completed" || status === "failed" || status === "cancelled") {
1474
+ this.trace("guard:release-lock", {
1475
+ conversationId,
1476
+ turnId: turn.id,
1477
+ status
1478
+ });
1479
+ this.releaseLock(conversationId);
1480
+ }
1481
+ }
1482
+ }
1483
+ });
1484
+ }
1485
+ acquireLock(conversationId) {
1486
+ const existing = this.conversationLocks.get(conversationId);
1487
+ if (existing?.timer) {
1488
+ clearTimeout(existing.timer);
1489
+ }
1490
+ const timer = setTimeout(() => {
1491
+ this.trace("guard:lock-timeout", { conversationId });
1492
+ this.conversationLocks.delete(conversationId);
1493
+ }, this.LOCK_TIMEOUT_MS);
1494
+ if (timer && typeof timer.unref === "function") {
1495
+ timer.unref();
1496
+ }
1497
+ this.conversationLocks.set(conversationId, { timer });
1498
+ }
1499
+ releaseLock(conversationId) {
1500
+ const existing = this.conversationLocks.get(conversationId);
1501
+ if (existing) {
1502
+ if (existing.timer) {
1503
+ clearTimeout(existing.timer);
1504
+ }
1505
+ this.conversationLocks.delete(conversationId);
1506
+ }
1507
+ }
1508
+ getConversationSnapshot(conversationId) {
1509
+ return this.getSnapshot().conversations.find(
1510
+ (conversation) => conversation.id === conversationId
1511
+ ) ?? null;
1512
+ }
1513
+ async waitForConversationSnapshot(conversationId) {
1514
+ const existing = this.getConversationSnapshot(conversationId);
1515
+ if (existing) return existing;
1516
+ return await new Promise((resolve3) => {
1517
+ let unsubscribe = null;
1518
+ const timeout = setTimeout(() => {
1519
+ unsubscribe?.();
1520
+ resolve3(this.getConversationSnapshot(conversationId));
1521
+ }, this.RECIPIENT_STATE_WAIT_MS);
1522
+ if (typeof timeout.unref === "function") {
1523
+ timeout.unref();
1524
+ }
1525
+ unsubscribe = this.subscribe((event) => {
1526
+ if (event.kind !== "conversation-state" || event.sourceAddress.conversationId !== conversationId) {
1527
+ return;
1528
+ }
1529
+ clearTimeout(timeout);
1530
+ unsubscribe?.();
1531
+ resolve3(
1532
+ event.snapshot.conversations.find(
1533
+ (conversation) => conversation.id === conversationId
1534
+ ) ?? this.getConversationSnapshot(conversationId)
1535
+ );
1536
+ });
1537
+ });
1538
+ }
1539
+ async assertRecipientCanStartTurn(conversationId, method) {
1540
+ const conversation = await this.waitForConversationSnapshot(conversationId);
1541
+ const lastStatus = extractConversationLastTurnStatus(conversation);
1542
+ if (lastStatus === "inProgress") {
1543
+ this.trace("guard:recipient-active-turn", {
1544
+ conversationId,
1545
+ method,
1546
+ lastStatus
1547
+ });
1548
+ throw new Error(
1549
+ `[Stability Guard] Recipient conversation "${conversationId}" has an active in-progress turn; refusing "${method}" to avoid a stuck nested turn.`
1550
+ );
1551
+ }
1552
+ }
1553
+ createConsentReceipt(options) {
1554
+ const targetAddress = this.resolveConversationTargetAddress(
1555
+ options.conversationId,
1556
+ {
1557
+ hostId: options.hostId ?? null,
1558
+ ownerClientId: options.ownerClientId ?? null
1559
+ }
1560
+ );
1561
+ const createOptions = {
1562
+ receiptsDir: this.receiptsDir,
1563
+ secretsDir: this.secretsDir,
1564
+ scope: options.scope ?? "drive",
1565
+ hostId: targetAddress.hostId,
1566
+ conversationId: options.conversationId,
1567
+ ownerClientId: targetAddress.ownerClientId,
1568
+ issuedByClientId: this.getOwnClientId(),
1569
+ ttlSeconds: options.ttlSeconds ?? this.defaultConsentTtlSeconds,
1570
+ allowedMethods: [...options.allowedMethods ?? []]
1571
+ };
1572
+ const created = createConsentReceipt(createOptions);
1573
+ writeConsentLedgerEvent({
1574
+ commsDir: this.commsDir,
1575
+ event: "issued",
1576
+ grantId: created.receipt.id,
1577
+ scope: created.receipt.scope,
1578
+ method: created.receipt.allowedMethods.length === 1 ? created.receipt.allowedMethods[0] : null,
1579
+ hostId: created.receipt.hostId,
1580
+ conversationId: created.receipt.conversationId,
1581
+ issuedAt: created.receipt.createdAt,
1582
+ expiresAt: created.receipt.expiresAt,
1583
+ result: "granted",
1584
+ requester: this.buildSourceAddress(options.conversationId, targetAddress),
1585
+ owner: targetAddress,
1586
+ issuedByClientId: created.receipt.issuedByClientId
1587
+ });
1588
+ return created;
1589
+ }
1590
+ createStartTurnSuggestion(options) {
1591
+ return this.createSuggestion({
1592
+ conversationId: options.conversationId,
1593
+ method: "thread-follower-start-turn",
1594
+ params: buildFollowerStartTurnParams(options),
1595
+ action: options.action ?? "start-turn",
1596
+ consentRef: options.consentRef ?? null
1597
+ });
1598
+ }
1599
+ createSuggestion(options) {
1600
+ const method = normalizeMethod(options.method);
1601
+ const targetAddress = this.resolveConversationTargetAddress(
1602
+ options.conversationId
1603
+ );
1604
+ const sourceAddress = this.buildSourceAddress(
1605
+ options.conversationId,
1606
+ targetAddress
1607
+ );
1608
+ return {
1609
+ id: randomUUID4(),
1610
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1611
+ status: "pending-owner-approval",
1612
+ scope: "suggest",
1613
+ method,
1614
+ action: normalizeActionLabel(options.action, method),
1615
+ conversationId: options.conversationId,
1616
+ payload: options.params ?? null,
1617
+ sourceAddress,
1618
+ targetAddress,
1619
+ consentRef: options.consentRef?.trim() || null
1620
+ };
1621
+ }
1622
+ async startTurn(options) {
1623
+ return this.driveAction({
1624
+ conversationId: options.conversationId,
1625
+ method: "thread-follower-start-turn",
1626
+ params: buildFollowerStartTurnParams(options),
1627
+ action: options.action ?? "start-turn",
1628
+ consentRef: options.consentRef ?? null,
1629
+ hostId: options.hostId ?? null,
1630
+ ownerClientId: options.ownerClientId ?? null
1631
+ });
1632
+ }
1633
+ async driveAction(options) {
1634
+ const method = normalizeMethod(options.method);
1635
+ const conversationId = options.conversationId.trim();
1636
+ const isGuarded = STABILITY_GUARDED_METHODS.has(method);
1637
+ const targetAddress = this.resolveConversationTargetAddress(
1638
+ conversationId,
1639
+ {
1640
+ hostId: options.hostId ?? null,
1641
+ ownerClientId: options.ownerClientId ?? null
1642
+ }
1643
+ );
1644
+ const ownerClientId = targetAddress.ownerClientId?.trim();
1645
+ if (!ownerClientId) {
1646
+ throw new Error(
1647
+ `Conversation "${conversationId}" does not have a live ownerClientId.`
1648
+ );
1649
+ }
1650
+ const sourceAddress = this.buildSourceAddress(
1651
+ conversationId,
1652
+ targetAddress
1653
+ );
1654
+ this.trace("drive:prepare", {
1655
+ conversationId,
1656
+ method,
1657
+ action: normalizeActionLabel(options.action, method),
1658
+ consentRef: options.consentRef ?? null,
1659
+ hostId: targetAddress.hostId,
1660
+ ownerClientId,
1661
+ ...summarizeDriveParams(options.params)
1662
+ });
1663
+ let preparedReceipt = null;
1664
+ let guardLockAcquired = false;
1665
+ try {
1666
+ preparedReceipt = prepareConsentReceipt({
1667
+ receiptsDir: this.receiptsDir,
1668
+ secretsDir: this.secretsDir,
1669
+ consentRef: options.consentRef ?? null,
1670
+ requiredScope: "drive",
1671
+ method,
1672
+ hostId: targetAddress.hostId,
1673
+ conversationId,
1674
+ ownerClientId,
1675
+ reservationOwnerId: this.reservationOwnerId
1676
+ });
1677
+ if (isGuarded) {
1678
+ await this.assertRecipientCanStartTurn(conversationId, method);
1679
+ if (this.conversationLocks.has(conversationId)) {
1680
+ this.trace("guard:locked", { conversationId, method });
1681
+ throw new Error(
1682
+ `[Stability Guard] Rejecting "${method}". Conversation "${conversationId}" has an active in-progress turn.`
1683
+ );
1684
+ }
1685
+ const now = Date.now();
1686
+ const lastDrive = this.conversationLastDriveTime.get(conversationId) ?? 0;
1687
+ const elapsed = now - lastDrive;
1688
+ if (elapsed < this.COOLDOWN_MS) {
1689
+ const waitTime = this.COOLDOWN_MS - elapsed;
1690
+ this.trace("guard:cooldown", {
1691
+ conversationId,
1692
+ method,
1693
+ remainingMs: waitTime
1694
+ });
1695
+ throw new Error(
1696
+ `[Stability Guard] Cooldown active for "${method}" on conversation "${conversationId}". Wait ${Math.ceil(waitTime / 1e3)}s.`
1697
+ );
1698
+ }
1699
+ this.acquireLock(conversationId);
1700
+ guardLockAcquired = true;
1701
+ }
1702
+ this.trace("drive:request", {
1703
+ conversationId,
1704
+ method,
1705
+ ownerClientId
1706
+ });
1707
+ const response = await this.sendRequest(
1708
+ method,
1709
+ options.params,
1710
+ ownerClientId
1711
+ );
1712
+ this.trace("drive:response", {
1713
+ conversationId,
1714
+ method,
1715
+ ownerClientId,
1716
+ turnId: extractDriveTurnId(response),
1717
+ resultType: response.resultType ?? null
1718
+ });
1719
+ preparedReceipt.commit();
1720
+ if (isGuarded) {
1721
+ this.conversationLastDriveTime.set(conversationId, Date.now());
1722
+ }
1723
+ const executedAt = (/* @__PURE__ */ new Date()).toISOString();
1724
+ writeConsentLedgerEvent({
1725
+ commsDir: this.commsDir,
1726
+ event: "consumed",
1727
+ grantId: preparedReceipt.receipt.id,
1728
+ scope: preparedReceipt.receipt.scope,
1729
+ method,
1730
+ hostId: targetAddress.hostId,
1731
+ conversationId,
1732
+ issuedAt: preparedReceipt.receipt.createdAt,
1733
+ expiresAt: preparedReceipt.receipt.expiresAt,
1734
+ consumedAt: executedAt,
1735
+ recordedAt: executedAt,
1736
+ result: "executed",
1737
+ requester: sourceAddress,
1738
+ owner: targetAddress,
1739
+ issuedByClientId: preparedReceipt.receipt.issuedByClientId
1740
+ });
1741
+ return {
1742
+ executedAt,
1743
+ scope: "drive",
1744
+ method,
1745
+ action: normalizeActionLabel(options.action, method),
1746
+ conversationId,
1747
+ sourceAddress,
1748
+ targetAddress,
1749
+ consentRef: preparedReceipt.receipt.id,
1750
+ receipt: preparedReceipt.receipt,
1751
+ response
1752
+ };
1753
+ } catch (error) {
1754
+ if (guardLockAcquired) this.releaseLock(conversationId);
1755
+ this.trace("drive:error", {
1756
+ conversationId,
1757
+ method,
1758
+ ownerClientId,
1759
+ error: error instanceof Error ? error.stack ?? error.message : String(error)
1760
+ });
1761
+ preparedReceipt?.abort();
1762
+ writeConsentLedgerEvent({
1763
+ commsDir: this.commsDir,
1764
+ event: "rejected",
1765
+ grantId: preparedReceipt?.receipt.id ?? options.consentRef ?? null,
1766
+ scope: preparedReceipt?.receipt.scope ?? "drive",
1767
+ method,
1768
+ hostId: targetAddress.hostId,
1769
+ conversationId,
1770
+ issuedAt: preparedReceipt?.receipt.createdAt ?? null,
1771
+ expiresAt: preparedReceipt?.receipt.expiresAt ?? null,
1772
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
1773
+ result: extractRejectionResult(error),
1774
+ requester: sourceAddress,
1775
+ owner: targetAddress,
1776
+ issuedByClientId: preparedReceipt?.receipt.issuedByClientId ?? this.getOwnClientId()
1777
+ });
1778
+ throw error;
1779
+ }
1780
+ }
1781
+ resolveConversationTargetAddress(conversationId, fallback) {
1782
+ const normalizedConversationId = conversationId.trim();
1783
+ if (!normalizedConversationId) {
1784
+ throw new Error(
1785
+ "Codex IPC control actions require a non-empty conversationId."
1786
+ );
1787
+ }
1788
+ const conversation = this.getSnapshot().conversations.find(
1789
+ (candidate) => candidate.id === normalizedConversationId
1790
+ );
1791
+ if (conversation) {
1792
+ return normalizeAddress2(conversation.address);
1793
+ }
1794
+ const ownerClientId = fallback?.ownerClientId?.trim() || null;
1795
+ const hostId = fallback?.hostId?.trim() || this.getHostId();
1796
+ if (!ownerClientId) {
1797
+ throw new Error(
1798
+ `Conversation "${normalizedConversationId}" is not present in the current observe snapshot.`
1799
+ );
1800
+ }
1801
+ return {
1802
+ hostId,
1803
+ clientId: ownerClientId,
1804
+ conversationId: normalizedConversationId,
1805
+ ownerClientId
1806
+ };
1807
+ }
1808
+ buildSourceAddress(conversationId, targetAddress) {
1809
+ return {
1810
+ hostId: this.getHostId(),
1811
+ clientId: this.getOwnClientId(),
1812
+ conversationId,
1813
+ ownerClientId: targetAddress.ownerClientId
1814
+ };
1815
+ }
1816
+ };
1817
+
1818
+ // src/bridges/codex-remote-ipc-relay.ts
1819
+ function normalizeString3(value) {
1820
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1821
+ }
1822
+ async function readStdin() {
1823
+ const chunks = [];
1824
+ for await (const chunk of process.stdin) {
1825
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1826
+ }
1827
+ return Buffer.concat(chunks).toString("utf8");
1828
+ }
1829
+ function writeResult(value) {
1830
+ process.stdout.write(`${JSON.stringify(value)}
1831
+ `);
1832
+ }
1833
+ function extractTurnId(response) {
1834
+ const responseRecord = response && typeof response === "object" && !Array.isArray(response) ? response : null;
1835
+ const payload = responseRecord?.result && typeof responseRecord.result === "object" && !Array.isArray(responseRecord.result) ? responseRecord.result : null;
1836
+ const direct = payload?.turn && typeof payload.turn === "object" && !Array.isArray(payload.turn) ? payload.turn.id : null;
1837
+ if (typeof direct === "string" && direct.trim()) {
1838
+ return direct.trim();
1839
+ }
1840
+ const nestedResult = payload?.result && typeof payload.result === "object" && !Array.isArray(payload.result) ? payload.result : null;
1841
+ const nested = nestedResult?.turn && typeof nestedResult.turn === "object" && !Array.isArray(nestedResult.turn) ? nestedResult.turn.id : null;
1842
+ return typeof nested === "string" && nested.trim() ? nested.trim() : null;
1843
+ }
1844
+ function summarizeId(value) {
1845
+ return value.length <= 16 ? value : `${value.slice(0, 8)}...${value.slice(-6)}`;
1846
+ }
1847
+ function hashTuple(values) {
1848
+ return createHash2("sha256").update(values.map((value) => value ?? "").join("\0")).digest("hex").slice(0, 16);
1849
+ }
1850
+ async function main() {
1851
+ const raw = await readStdin();
1852
+ const request = JSON.parse(raw);
1853
+ const conversationId = normalizeString3(request.conversationId);
1854
+ const ownerClientId = normalizeString3(request.ownerClientId);
1855
+ const text = normalizeString3(request.text);
1856
+ if (!conversationId || !ownerClientId || !text) {
1857
+ writeResult({
1858
+ ok: false,
1859
+ error: "codex remote relay requires conversationId, ownerClientId, and text"
1860
+ });
1861
+ process.exitCode = 2;
1862
+ return;
1863
+ }
1864
+ const transport = new ExperimentalCodexIpcControlTransport({
1865
+ commsDir: normalizeString3(request.commsDir) ?? void 0,
1866
+ hostId: normalizeString3(request.hostId)
1867
+ });
1868
+ const hostId = normalizeString3(request.hostId);
1869
+ try {
1870
+ await transport.connect();
1871
+ const created = transport.createConsentReceipt({
1872
+ conversationId,
1873
+ hostId: normalizeString3(request.hostId),
1874
+ ownerClientId,
1875
+ allowedMethods: ["thread-follower-start-turn"]
1876
+ });
1877
+ const result = await transport.startTurn({
1878
+ conversationId,
1879
+ text,
1880
+ consentRef: created.receipt.id,
1881
+ hostId,
1882
+ ownerClientId,
1883
+ action: "start-turn"
1884
+ });
1885
+ writeResult({
1886
+ ok: true,
1887
+ adapter: "ssh-ipc-relay",
1888
+ hostId,
1889
+ tupleHash: hashTuple([hostId, conversationId, ownerClientId]),
1890
+ conversationId: summarizeId(conversationId),
1891
+ ownerClientId: summarizeId(ownerClientId),
1892
+ turnId: extractTurnId(result.response),
1893
+ consentRef: created.receipt.id
1894
+ });
1895
+ } catch (error) {
1896
+ writeResult({
1897
+ ok: false,
1898
+ error: error instanceof Error ? error.message : String(error)
1899
+ });
1900
+ process.exitCode = 1;
1901
+ } finally {
1902
+ await transport.disconnect().catch(() => void 0);
1903
+ }
1904
+ }
1905
+ main().catch((error) => {
1906
+ writeResult({
1907
+ ok: false,
1908
+ error: error instanceof Error ? error.message : String(error)
1909
+ });
1910
+ process.exitCode = 1;
1911
+ });
1912
+ //# sourceMappingURL=codex-remote-ipc-relay.mjs.map