@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
|
@@ -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
|