agent-embassy 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/CONTRIBUTING.md +11 -3
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/SECURITY.md +30 -9
- package/dist/src/gateway/cli.d.ts +1 -1
- package/dist/src/gateway/cli.js +4 -1
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +43 -2
- package/dist/src/gateway/codex-app-server.js +539 -3
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +31 -3
- package/dist/src/gateway/compatibility.js +100 -29
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.d.ts +1 -1
- package/dist/src/gateway/dashboard-copy.en.d.ts +6 -0
- package/dist/src/gateway/dashboard-copy.en.js +6 -0
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +6 -0
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +6 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js +6 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +27 -7
- package/dist/src/gateway/dashboard-model.js +78 -25
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.d.ts +3 -0
- package/dist/src/gateway/dashboard.js +15 -3
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/deepseek-detect.d.ts +13 -0
- package/dist/src/gateway/deepseek-detect.js +125 -0
- package/dist/src/gateway/deepseek-detect.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +12 -11
- package/dist/src/gateway/providers.d.ts +13 -2
- package/dist/src/gateway/providers.js +370 -24
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +2 -0
- package/dist/src/gateway/server.js +6 -0
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +21 -2
- package/dist/src/gateway/service.js +122 -38
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/types.d.ts +4 -3
- package/dist/src/gateway/types.js +17 -14
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +2 -2
- package/docs/CONFIGURATION.zh-CN.md +2 -2
- package/docs/DASHBOARD.md +10 -4
- package/docs/DASHBOARD.zh-CN.md +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +40 -14
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
2
4
|
import WebSocket from "ws";
|
|
3
5
|
import { sharesCompatibilityMajor } from "./compatibility.js";
|
|
4
6
|
/**
|
|
@@ -9,15 +11,33 @@ import { sharesCompatibilityMajor } from "./compatibility.js";
|
|
|
9
11
|
* authorizes an interrupt or a generic/public JSON-RPC escape hatch.
|
|
10
12
|
*/
|
|
11
13
|
export const CODEX_APP_SERVER_V1_METHODS = [
|
|
14
|
+
"account/rateLimits/read",
|
|
15
|
+
"model/list",
|
|
16
|
+
"thread/archive",
|
|
12
17
|
"thread/loaded/list",
|
|
13
18
|
"thread/resume",
|
|
19
|
+
"thread/start",
|
|
14
20
|
"thread/unsubscribe",
|
|
15
21
|
"turn/start",
|
|
16
22
|
"turn/steer",
|
|
17
23
|
"turn/interrupt",
|
|
18
24
|
];
|
|
25
|
+
export const CODEX_PROBE_MODEL_PREFERENCE = ["gpt-5.6-luna"];
|
|
26
|
+
export const CODEX_PROBE_EFFORT = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
27
|
+
export const CODEX_WRITE_PROBE_INPUT = "Reply with exactly OK. Do not use tools.";
|
|
19
28
|
/** Exact App Server builds whose writable v2 schema was reviewed. */
|
|
20
29
|
export const CODEX_APP_SERVER_WRITABLE_VERSIONS = ["0.147.0"];
|
|
30
|
+
export function codexWriteProbeFailure(safeErrorCode, evidence = {}) {
|
|
31
|
+
return {
|
|
32
|
+
...(evidence.archivedThreadCount === 1 ? { archivedThreadCount: 1 } : {}),
|
|
33
|
+
outcome: "fail",
|
|
34
|
+
safeErrorCode,
|
|
35
|
+
settingsEchoObserved: evidence.settingsEchoObserved ?? false,
|
|
36
|
+
...(evidence.tokenCount === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { tokenCount: evidence.tokenCount }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
21
41
|
export class CodexConnectorError extends Error {
|
|
22
42
|
ambiguous;
|
|
23
43
|
code;
|
|
@@ -243,6 +263,9 @@ export class WebSocketDuplexTransport {
|
|
|
243
263
|
}
|
|
244
264
|
}
|
|
245
265
|
const V1_METHOD_SET = new Set(CODEX_APP_SERVER_V1_METHODS);
|
|
266
|
+
const PROBE_ONLY_METHODS = new Set([
|
|
267
|
+
"account/rateLimits/read", "model/list", "thread/archive", "thread/start",
|
|
268
|
+
]);
|
|
246
269
|
const OUTPUT_NOTIFICATION_OPT_OUTS = [
|
|
247
270
|
"item/started",
|
|
248
271
|
"item/agentMessage/delta",
|
|
@@ -252,6 +275,14 @@ const OUTPUT_NOTIFICATION_OPT_OUTS = [
|
|
|
252
275
|
"turn/diff/updated",
|
|
253
276
|
"turn/plan/updated",
|
|
254
277
|
];
|
|
278
|
+
const PROBE_PASSIVE_ITEM_TYPES = new Set(["agentMessage", "reasoning", "userMessage"]);
|
|
279
|
+
const PROBE_TOOL_NOTIFICATION_METHODS = new Set([
|
|
280
|
+
"item/autoApprovalReview/completed", "item/autoApprovalReview/started",
|
|
281
|
+
"item/commandExecution/terminalInteraction",
|
|
282
|
+
"item/fileChange/outputDelta", "item/fileChange/patchUpdated",
|
|
283
|
+
"item/mcpToolCall/progress",
|
|
284
|
+
]);
|
|
285
|
+
const MAX_PROBE_TOKEN_COUNT = 1_000_000_000;
|
|
255
286
|
const APPROVAL_REQUEST_METHODS = new Set([
|
|
256
287
|
"item/commandExecution/requestApproval",
|
|
257
288
|
"item/fileChange/requestApproval",
|
|
@@ -270,6 +301,56 @@ function validOpaqueId(value, maximumLength = 256) {
|
|
|
270
301
|
value.length <= maximumLength &&
|
|
271
302
|
/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value));
|
|
272
303
|
}
|
|
304
|
+
function validUuidV7(value) {
|
|
305
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value);
|
|
306
|
+
}
|
|
307
|
+
function parseLoadedThreadIds(value) {
|
|
308
|
+
if (!isRecord(value) ||
|
|
309
|
+
!Array.isArray(value.data) ||
|
|
310
|
+
(value.nextCursor !== undefined && value.nextCursor !== null)) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (value.data.length > 100_000 ||
|
|
314
|
+
!value.data.every((threadId) => typeof threadId === "string" && validOpaqueId(threadId)) ||
|
|
315
|
+
new Set(value.data).size !== value.data.length) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
return value.data;
|
|
319
|
+
}
|
|
320
|
+
function rateLimitConstrained(value) {
|
|
321
|
+
if (!isRecord(value) || !isRecord(value.rateLimits))
|
|
322
|
+
return null;
|
|
323
|
+
const byLimit = value.rateLimitsByLimitId;
|
|
324
|
+
const snapshot = isRecord(byLimit) && isRecord(byLimit.codex)
|
|
325
|
+
? byLimit.codex
|
|
326
|
+
: value.rateLimits;
|
|
327
|
+
const percent = (container, key) => {
|
|
328
|
+
if (container === null || container === undefined)
|
|
329
|
+
return Number.NaN;
|
|
330
|
+
if (!isRecord(container) || !Number.isSafeInteger(container[key]))
|
|
331
|
+
return null;
|
|
332
|
+
const result = container[key];
|
|
333
|
+
return result >= 0 && result <= 100 ? result : null;
|
|
334
|
+
};
|
|
335
|
+
const remaining = percent(snapshot.individualLimit, "remainingPercent");
|
|
336
|
+
const primary = percent(snapshot.primary, "usedPercent");
|
|
337
|
+
const secondary = percent(snapshot.secondary, "usedPercent");
|
|
338
|
+
if ([remaining, primary, secondary].includes(null))
|
|
339
|
+
return null;
|
|
340
|
+
if ((snapshot.rateLimitReachedType !== null &&
|
|
341
|
+
snapshot.rateLimitReachedType !== undefined) ||
|
|
342
|
+
snapshot.spendControlReached === true) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (snapshot.spendControlReached !== null &&
|
|
346
|
+
snapshot.spendControlReached !== undefined &&
|
|
347
|
+
snapshot.spendControlReached !== false) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return ((!Number.isNaN(remaining) && remaining <= 5) ||
|
|
351
|
+
(!Number.isNaN(primary) && primary >= 95) ||
|
|
352
|
+
(!Number.isNaN(secondary) && secondary >= 95));
|
|
353
|
+
}
|
|
273
354
|
function validateRoute(route) {
|
|
274
355
|
if (!validOpaqueId(route.threadId) ||
|
|
275
356
|
!validOpaqueId(route.endpointGeneration, 128)) {
|
|
@@ -359,6 +440,8 @@ export class CodexAppServerConnector {
|
|
|
359
440
|
nextRequestId = 1;
|
|
360
441
|
ownsActiveTurn = false;
|
|
361
442
|
pending = new Map();
|
|
443
|
+
probeAttempted = false;
|
|
444
|
+
probeRuntime = null;
|
|
362
445
|
queue = [];
|
|
363
446
|
revision = 0;
|
|
364
447
|
routeStatus = "unknown";
|
|
@@ -383,6 +466,7 @@ export class CodexAppServerConnector {
|
|
|
383
466
|
turnWatchdogMs;
|
|
384
467
|
route;
|
|
385
468
|
transport;
|
|
469
|
+
writeCompatibilityProbe;
|
|
386
470
|
writesEnabled;
|
|
387
471
|
constructor(options) {
|
|
388
472
|
this.route = validateRoute(options.route);
|
|
@@ -391,15 +475,18 @@ export class CodexAppServerConnector {
|
|
|
391
475
|
throw new CodexConnectorError("INVALID_CONFIGURATION");
|
|
392
476
|
}
|
|
393
477
|
if (options.writesEnabled &&
|
|
394
|
-
!CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(options.compatibility.appServerVersion)
|
|
478
|
+
!(CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(options.compatibility.appServerVersion) ||
|
|
479
|
+
(options.compatibility.observedSchemaCandidate === true &&
|
|
480
|
+
!options.compatibility.appServerVersion.includes("-")))) {
|
|
395
481
|
throw new CodexConnectorError("INVALID_CONFIGURATION");
|
|
396
482
|
}
|
|
397
483
|
this.writesEnabled = options.writesEnabled;
|
|
484
|
+
this.writeCompatibilityProbe = options.writeCompatibilityProbe === true;
|
|
398
485
|
this.transport = options.transport;
|
|
399
486
|
this.clientInfo = validateClientInfo(options.clientInfo ?? {
|
|
400
487
|
name: "agent_embassy_gateway",
|
|
401
488
|
title: "Embassy Gateway",
|
|
402
|
-
version: "1.
|
|
489
|
+
version: "1.6.1",
|
|
403
490
|
});
|
|
404
491
|
this.maxFrameBytes = positiveInteger(options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES, "INVALID_CONFIGURATION");
|
|
405
492
|
this.maxDeadlineMs = positiveInteger(options.maxDeadlineMs ?? DEFAULT_MAX_DEADLINE_MS, "INVALID_CONFIGURATION");
|
|
@@ -709,6 +796,309 @@ export class CodexAppServerConnector {
|
|
|
709
796
|
this.handleDisconnect();
|
|
710
797
|
}
|
|
711
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Run the one fixed disposable-thread write probe. The method never exposes
|
|
801
|
+
* JSON-RPC, native IDs, content, or route state, and every failure settles as
|
|
802
|
+
* a safe result so compatibility discovery cannot take down the broker.
|
|
803
|
+
*/
|
|
804
|
+
async runWriteCompatibilityProbe(input) {
|
|
805
|
+
if (this.probeAttempted) {
|
|
806
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
807
|
+
}
|
|
808
|
+
this.probeAttempted = true;
|
|
809
|
+
let threadId = null;
|
|
810
|
+
let archivedThreadCount;
|
|
811
|
+
let result = codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
812
|
+
try {
|
|
813
|
+
if (!this.writeCompatibilityProbe ||
|
|
814
|
+
this.connection !== "ready" ||
|
|
815
|
+
this.inFlightMethod !== null ||
|
|
816
|
+
this.pending.size !== 0 ||
|
|
817
|
+
!isAbsolute(input.cwd) ||
|
|
818
|
+
!Array.isArray(input.forbiddenThreadIds) ||
|
|
819
|
+
!input.forbiddenThreadIds.every((value) => typeof value === "string" && validOpaqueId(value))) {
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
const beforeStat = await stat(input.cwd, { bigint: true });
|
|
823
|
+
const beforeEntries = await readdir(input.cwd);
|
|
824
|
+
if (!beforeStat.isDirectory() ||
|
|
825
|
+
beforeStat.uid !== BigInt(process.getuid?.() ?? -1) ||
|
|
826
|
+
(beforeStat.mode & 511n) !== 448n ||
|
|
827
|
+
beforeEntries.length !== 0) {
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
830
|
+
const limits = rateLimitConstrained(await this.request("account/rateLimits/read", null));
|
|
831
|
+
if (limits === null)
|
|
832
|
+
return result;
|
|
833
|
+
if (limits) {
|
|
834
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_RATE_LIMIT_CONSTRAINED");
|
|
835
|
+
}
|
|
836
|
+
const selection = this.selectProbeModel(await this.request("model/list", {
|
|
837
|
+
includeHidden: true,
|
|
838
|
+
limit: 100,
|
|
839
|
+
}).catch(() => null));
|
|
840
|
+
if (selection === null) {
|
|
841
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_MODEL_PIN_UNAVAILABLE");
|
|
842
|
+
}
|
|
843
|
+
const [model, effort] = selection;
|
|
844
|
+
const loadedBefore = parseLoadedThreadIds(await this.request("thread/loaded/list", {}));
|
|
845
|
+
if (loadedBefore === null)
|
|
846
|
+
return result;
|
|
847
|
+
const runtime = {
|
|
848
|
+
expectedCwd: input.cwd,
|
|
849
|
+
expectedEffort: effort,
|
|
850
|
+
expectedModel: model,
|
|
851
|
+
failCode: null,
|
|
852
|
+
phase: "awaiting_start",
|
|
853
|
+
resolveTurn: null,
|
|
854
|
+
settingsEchoObserved: false,
|
|
855
|
+
settingsThreadId: null,
|
|
856
|
+
threadId: null,
|
|
857
|
+
tokenCount: null,
|
|
858
|
+
turnId: null,
|
|
859
|
+
};
|
|
860
|
+
this.probeRuntime = runtime;
|
|
861
|
+
const started = await this.request("thread/start", {
|
|
862
|
+
allowProviderModelFallback: false,
|
|
863
|
+
approvalPolicy: "never",
|
|
864
|
+
cwd: input.cwd,
|
|
865
|
+
dynamicTools: [],
|
|
866
|
+
environments: [],
|
|
867
|
+
ephemeral: false,
|
|
868
|
+
model,
|
|
869
|
+
runtimeWorkspaceRoots: [],
|
|
870
|
+
sandbox: "read-only",
|
|
871
|
+
selectedCapabilityRoots: [],
|
|
872
|
+
});
|
|
873
|
+
const candidateThreadId = this.probeCleanupThreadId(started, loadedBefore, input.forbiddenThreadIds);
|
|
874
|
+
if (candidateThreadId === null ||
|
|
875
|
+
!this.validateProbeThread(started, input.cwd, model, candidateThreadId)) {
|
|
876
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
|
|
877
|
+
}
|
|
878
|
+
threadId = candidateThreadId;
|
|
879
|
+
runtime.threadId = candidateThreadId;
|
|
880
|
+
if (runtime.settingsThreadId !== null &&
|
|
881
|
+
runtime.settingsThreadId !== candidateThreadId) {
|
|
882
|
+
runtime.failCode = "CODEX_WRITE_PROBE_THREAD_SETUP_FAILED";
|
|
883
|
+
}
|
|
884
|
+
if (runtime.failCode === null) {
|
|
885
|
+
const turnResult = await this.request("turn/start", {
|
|
886
|
+
approvalPolicy: "never",
|
|
887
|
+
cwd: input.cwd,
|
|
888
|
+
effort,
|
|
889
|
+
environments: [],
|
|
890
|
+
input: [{ text: CODEX_WRITE_PROBE_INPUT, type: "text" }],
|
|
891
|
+
model,
|
|
892
|
+
runtimeWorkspaceRoots: [],
|
|
893
|
+
sandboxPolicy: { networkAccess: false, type: "readOnly" },
|
|
894
|
+
threadId,
|
|
895
|
+
});
|
|
896
|
+
const turn = isRecord(turnResult) ? parseTurn(turnResult.turn) : null;
|
|
897
|
+
if (turn === null ||
|
|
898
|
+
!isRecord(turnResult) ||
|
|
899
|
+
!isRecord(turnResult.turn) ||
|
|
900
|
+
!Array.isArray(turnResult.turn.items) ||
|
|
901
|
+
(turn.status !== "inProgress" && turn.status !== "completed")) {
|
|
902
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
|
|
903
|
+
}
|
|
904
|
+
if (runtime.turnId !== null && runtime.turnId !== turn.id) {
|
|
905
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
|
|
906
|
+
}
|
|
907
|
+
runtime.turnId = turn.id;
|
|
908
|
+
await this.waitForProbeTurn(runtime);
|
|
909
|
+
}
|
|
910
|
+
if (runtime.failCode !== null) {
|
|
911
|
+
result = codexWriteProbeFailure(runtime.failCode, {
|
|
912
|
+
settingsEchoObserved: runtime.settingsEchoObserved,
|
|
913
|
+
...(runtime.tokenCount === null
|
|
914
|
+
? {}
|
|
915
|
+
: { tokenCount: runtime.tokenCount }),
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
else if (runtime.phase === "terminal" &&
|
|
919
|
+
runtime.tokenCount !== null) {
|
|
920
|
+
const afterStat = await stat(input.cwd, { bigint: true });
|
|
921
|
+
const afterEntries = await readdir(input.cwd);
|
|
922
|
+
if (afterEntries.length === 0 &&
|
|
923
|
+
afterStat.dev === beforeStat.dev &&
|
|
924
|
+
afterStat.ino === beforeStat.ino &&
|
|
925
|
+
afterStat.mtimeNs === beforeStat.mtimeNs &&
|
|
926
|
+
afterStat.uid === beforeStat.uid &&
|
|
927
|
+
afterStat.mode === beforeStat.mode) {
|
|
928
|
+
result = {
|
|
929
|
+
archivedThreadCount: 1,
|
|
930
|
+
outcome: "pass",
|
|
931
|
+
settingsEchoObserved: runtime.settingsEchoObserved,
|
|
932
|
+
tokenCount: runtime.tokenCount,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
result = codexWriteProbeFailure("CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED", {
|
|
937
|
+
settingsEchoObserved: runtime.settingsEchoObserved,
|
|
938
|
+
tokenCount: runtime.tokenCount,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
const normalized = this.normalizeError(error, true);
|
|
945
|
+
const runtime = this.probeRuntime;
|
|
946
|
+
result = codexWriteProbeFailure(normalized.code === "REQUEST_TIMEOUT"
|
|
947
|
+
? "CODEX_WRITE_PROBE_TIMEOUT"
|
|
948
|
+
: "CODEX_WRITE_PROBE_THREAD_SETUP_FAILED", {
|
|
949
|
+
settingsEchoObserved: runtime?.settingsEchoObserved ?? false,
|
|
950
|
+
...(runtime?.tokenCount === null || runtime?.tokenCount === undefined
|
|
951
|
+
? {}
|
|
952
|
+
: { tokenCount: runtime.tokenCount }),
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
finally {
|
|
956
|
+
const runtime = this.probeRuntime;
|
|
957
|
+
if (threadId !== null &&
|
|
958
|
+
runtime !== null &&
|
|
959
|
+
runtime.turnId !== null &&
|
|
960
|
+
(result.outcome === "fail" || runtime.failCode !== null)) {
|
|
961
|
+
try {
|
|
962
|
+
await this.request("turn/interrupt", {
|
|
963
|
+
threadId,
|
|
964
|
+
turnId: runtime.turnId,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
// Archival remains the one required cleanup proof below.
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
let cleanupUnconfirmed = runtime !== null && threadId === null;
|
|
972
|
+
if (threadId !== null) {
|
|
973
|
+
try {
|
|
974
|
+
const archived = await this.request("thread/archive", { threadId });
|
|
975
|
+
if (!isRecord(archived) || Object.keys(archived).length !== 0) {
|
|
976
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
|
|
977
|
+
}
|
|
978
|
+
archivedThreadCount = 1;
|
|
979
|
+
const unsubscribed = await this.request("thread/unsubscribe", {
|
|
980
|
+
threadId,
|
|
981
|
+
});
|
|
982
|
+
if (!isRecord(unsubscribed) ||
|
|
983
|
+
(unsubscribed.status !== "unsubscribed" &&
|
|
984
|
+
unsubscribed.status !== "notSubscribed" &&
|
|
985
|
+
unsubscribed.status !== "notLoaded")) {
|
|
986
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
|
|
987
|
+
}
|
|
988
|
+
const loadedAfter = parseLoadedThreadIds(await this.request("thread/loaded/list", {}));
|
|
989
|
+
if (loadedAfter === null || loadedAfter.includes(threadId)) {
|
|
990
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
cleanupUnconfirmed = true;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const tokenCount = runtime?.tokenCount ?? result.tokenCount;
|
|
998
|
+
const evidence = {
|
|
999
|
+
...(archivedThreadCount === undefined ? {} : { archivedThreadCount }),
|
|
1000
|
+
settingsEchoObserved: runtime?.settingsEchoObserved ?? result.settingsEchoObserved,
|
|
1001
|
+
...(tokenCount === undefined ? {} : { tokenCount }),
|
|
1002
|
+
};
|
|
1003
|
+
if (cleanupUnconfirmed || (result.outcome === "pass" && archivedThreadCount !== 1)) {
|
|
1004
|
+
result = codexWriteProbeFailure("CODEX_WRITE_PROBE_CLEANUP_UNCONFIRMED", evidence);
|
|
1005
|
+
}
|
|
1006
|
+
else if (runtime?.failCode !== null && runtime?.failCode !== undefined) {
|
|
1007
|
+
result = codexWriteProbeFailure(runtime.failCode, evidence);
|
|
1008
|
+
}
|
|
1009
|
+
if (result.outcome === "fail") {
|
|
1010
|
+
result = codexWriteProbeFailure(result.safeErrorCode, evidence);
|
|
1011
|
+
}
|
|
1012
|
+
this.probeRuntime = null;
|
|
1013
|
+
}
|
|
1014
|
+
return result;
|
|
1015
|
+
}
|
|
1016
|
+
selectProbeModel(value) {
|
|
1017
|
+
if (!isRecord(value) || !Array.isArray(value.data) || value.data.length > 100) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
const data = value.data;
|
|
1021
|
+
for (const model of CODEX_PROBE_MODEL_PREFERENCE) {
|
|
1022
|
+
const candidate = data.find((entry) => isRecord(entry) &&
|
|
1023
|
+
entry.model === model &&
|
|
1024
|
+
entry.hidden === false);
|
|
1025
|
+
if (!isRecord(candidate) || !Array.isArray(candidate.supportedReasoningEfforts)) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
const supported = candidate.supportedReasoningEfforts;
|
|
1029
|
+
const effort = CODEX_PROBE_EFFORT.find((preference) => supported.some((option) => isRecord(option) && option.reasoningEffort === preference));
|
|
1030
|
+
if (effort !== undefined)
|
|
1031
|
+
return [model, effort];
|
|
1032
|
+
}
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
probeCleanupThreadId(value, loadedBefore, forbiddenThreadIds) {
|
|
1036
|
+
if (!isRecord(value) ||
|
|
1037
|
+
!isRecord(value.thread) ||
|
|
1038
|
+
typeof value.thread.id !== "string" ||
|
|
1039
|
+
!validUuidV7(value.thread.id) ||
|
|
1040
|
+
value.thread.id === this.route.threadId ||
|
|
1041
|
+
loadedBefore.includes(value.thread.id) ||
|
|
1042
|
+
forbiddenThreadIds.includes(value.thread.id)) {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
return value.thread.id;
|
|
1046
|
+
}
|
|
1047
|
+
validateProbeThread(value, cwd, model, threadId) {
|
|
1048
|
+
if (!isRecord(value) || !isRecord(value.thread))
|
|
1049
|
+
return false;
|
|
1050
|
+
const roots = value.runtimeWorkspaceRoots;
|
|
1051
|
+
const networkAccess = isRecord(value.sandbox)
|
|
1052
|
+
? value.sandbox.networkAccess
|
|
1053
|
+
: undefined;
|
|
1054
|
+
return (value.approvalPolicy === "never" &&
|
|
1055
|
+
value.cwd === cwd &&
|
|
1056
|
+
value.model === model &&
|
|
1057
|
+
isRecord(value.sandbox) &&
|
|
1058
|
+
value.sandbox.type === "readOnly" &&
|
|
1059
|
+
(networkAccess === undefined || networkAccess === false) &&
|
|
1060
|
+
(roots === undefined || (Array.isArray(roots) && roots.length === 0)) &&
|
|
1061
|
+
value.thread.id === threadId &&
|
|
1062
|
+
value.thread.cwd === cwd &&
|
|
1063
|
+
value.thread.ephemeral === false &&
|
|
1064
|
+
isRecord(value.thread.status) &&
|
|
1065
|
+
value.thread.status.type === "idle" &&
|
|
1066
|
+
Array.isArray(value.thread.turns) &&
|
|
1067
|
+
value.thread.turns.length === 0);
|
|
1068
|
+
}
|
|
1069
|
+
async waitForProbeTurn(runtime) {
|
|
1070
|
+
if (runtime.failCode !== null ||
|
|
1071
|
+
(runtime.phase === "terminal" && runtime.tokenCount !== null)) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
let timer;
|
|
1075
|
+
await new Promise((resolve) => {
|
|
1076
|
+
runtime.resolveTurn = resolve;
|
|
1077
|
+
timer = setTimeout(resolve, this.turnWatchdogMs);
|
|
1078
|
+
});
|
|
1079
|
+
runtime.resolveTurn = null;
|
|
1080
|
+
if (timer !== undefined)
|
|
1081
|
+
clearTimeout(timer);
|
|
1082
|
+
if (runtime.failCode === null &&
|
|
1083
|
+
!(runtime.phase === "terminal" && runtime.tokenCount !== null)) {
|
|
1084
|
+
runtime.failCode = "CODEX_WRITE_PROBE_TIMEOUT";
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
settleProbeRuntime() {
|
|
1088
|
+
const runtime = this.probeRuntime;
|
|
1089
|
+
if (runtime === null || runtime.resolveTurn === null)
|
|
1090
|
+
return;
|
|
1091
|
+
if (runtime.failCode !== null ||
|
|
1092
|
+
(runtime.phase === "terminal" && runtime.tokenCount !== null)) {
|
|
1093
|
+
runtime.resolveTurn();
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
settleProbeConnectionLoss() {
|
|
1097
|
+
if (this.probeRuntime === null)
|
|
1098
|
+
return;
|
|
1099
|
+
this.probeRuntime.failCode ??= "CODEX_WRITE_PROBE_THREAD_SETUP_FAILED";
|
|
1100
|
+
this.settleProbeRuntime();
|
|
1101
|
+
}
|
|
712
1102
|
async initialize() {
|
|
713
1103
|
try {
|
|
714
1104
|
const result = await this.request("initialize", {
|
|
@@ -717,7 +1107,7 @@ export class CodexAppServerConnector {
|
|
|
717
1107
|
// in the exactly attested 0.147.0 schema. The client method allowlist
|
|
718
1108
|
// remains closed and exposes no experimental method.
|
|
719
1109
|
experimentalApi: true,
|
|
720
|
-
optOutNotificationMethods:
|
|
1110
|
+
optOutNotificationMethods: OUTPUT_NOTIFICATION_OPT_OUTS.filter((method) => method !== "item/started" || !this.writeCompatibilityProbe),
|
|
721
1111
|
},
|
|
722
1112
|
clientInfo: this.clientInfo,
|
|
723
1113
|
});
|
|
@@ -1085,6 +1475,11 @@ export class CodexAppServerConnector {
|
|
|
1085
1475
|
if (method !== "initialize" && !V1_METHOD_SET.has(method)) {
|
|
1086
1476
|
return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
|
|
1087
1477
|
}
|
|
1478
|
+
if (method !== "initialize" &&
|
|
1479
|
+
PROBE_ONLY_METHODS.has(method) &&
|
|
1480
|
+
!this.writeCompatibilityProbe) {
|
|
1481
|
+
return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
|
|
1482
|
+
}
|
|
1088
1483
|
if (method === "initialize") {
|
|
1089
1484
|
if (this.initializeRequested || this.connection !== "connecting") {
|
|
1090
1485
|
return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
|
|
@@ -1201,6 +1596,8 @@ export class CodexAppServerConnector {
|
|
|
1201
1596
|
pending.resolve(parsed.result);
|
|
1202
1597
|
}
|
|
1203
1598
|
handleNotification(method, params) {
|
|
1599
|
+
if (this.handleProbeNotification(method, params))
|
|
1600
|
+
return;
|
|
1204
1601
|
if (method === "item/completed") {
|
|
1205
1602
|
this.handleCompletedItem(params);
|
|
1206
1603
|
return;
|
|
@@ -1430,6 +1827,14 @@ export class CodexAppServerConnector {
|
|
|
1430
1827
|
// item/model/tool payloads are neither normalized nor retained.
|
|
1431
1828
|
}
|
|
1432
1829
|
handleServerRequest(method, params) {
|
|
1830
|
+
if (this.probeRuntime !== null &&
|
|
1831
|
+
isRecord(params) &&
|
|
1832
|
+
params.threadId === this.probeRuntime.threadId) {
|
|
1833
|
+
this.probeRuntime.failCode ??=
|
|
1834
|
+
"CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED";
|
|
1835
|
+
this.settleProbeRuntime();
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1433
1838
|
if (!APPROVAL_REQUEST_METHODS.has(method)) {
|
|
1434
1839
|
this.emit("server_request_ignored");
|
|
1435
1840
|
return;
|
|
@@ -1454,6 +1859,135 @@ export class CodexAppServerConnector {
|
|
|
1454
1859
|
// Intentionally no JSON-RPC response: the gateway is not an approval UI or
|
|
1455
1860
|
// authority. The owning Desktop client must resolve the request.
|
|
1456
1861
|
}
|
|
1862
|
+
handleProbeNotification(method, params) {
|
|
1863
|
+
const runtime = this.probeRuntime;
|
|
1864
|
+
if (runtime === null)
|
|
1865
|
+
return false;
|
|
1866
|
+
const fail = (code) => {
|
|
1867
|
+
runtime.failCode ??= code;
|
|
1868
|
+
this.settleProbeRuntime();
|
|
1869
|
+
};
|
|
1870
|
+
if (method === "model/rerouted") {
|
|
1871
|
+
fail("CODEX_WRITE_PROBE_MODEL_REROUTED");
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
if (!isRecord(params))
|
|
1875
|
+
return false;
|
|
1876
|
+
const observeTurnId = (value) => {
|
|
1877
|
+
if (typeof value !== "string" || !validOpaqueId(value))
|
|
1878
|
+
return false;
|
|
1879
|
+
if (runtime.turnId !== null && runtime.turnId !== value)
|
|
1880
|
+
return false;
|
|
1881
|
+
runtime.turnId = value;
|
|
1882
|
+
return true;
|
|
1883
|
+
};
|
|
1884
|
+
if (method === "thread/settings/updated") {
|
|
1885
|
+
runtime.settingsEchoObserved = true;
|
|
1886
|
+
const settingsThreadId = params.threadId;
|
|
1887
|
+
const settings = params.threadSettings;
|
|
1888
|
+
const sandbox = isRecord(settings) ? settings.sandboxPolicy : null;
|
|
1889
|
+
if (typeof settingsThreadId !== "string" ||
|
|
1890
|
+
!validUuidV7(settingsThreadId) ||
|
|
1891
|
+
(runtime.threadId !== null && runtime.threadId !== settingsThreadId) ||
|
|
1892
|
+
(runtime.settingsThreadId !== null &&
|
|
1893
|
+
runtime.settingsThreadId !== settingsThreadId) ||
|
|
1894
|
+
!isRecord(settings) ||
|
|
1895
|
+
settings.cwd !== runtime.expectedCwd ||
|
|
1896
|
+
settings.model !== runtime.expectedModel ||
|
|
1897
|
+
settings.approvalPolicy !== "never" ||
|
|
1898
|
+
(settings.effort !== undefined &&
|
|
1899
|
+
settings.effort !== runtime.expectedEffort) ||
|
|
1900
|
+
!isRecord(sandbox) ||
|
|
1901
|
+
sandbox.type !== "readOnly" ||
|
|
1902
|
+
(sandbox.networkAccess !== undefined &&
|
|
1903
|
+
sandbox.networkAccess !== false)) {
|
|
1904
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1905
|
+
}
|
|
1906
|
+
else {
|
|
1907
|
+
runtime.settingsThreadId = settingsThreadId;
|
|
1908
|
+
}
|
|
1909
|
+
return true;
|
|
1910
|
+
}
|
|
1911
|
+
if (runtime.threadId === null || params.threadId !== runtime.threadId) {
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
if (PROBE_TOOL_NOTIFICATION_METHODS.has(method)) {
|
|
1915
|
+
fail("CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED");
|
|
1916
|
+
return true;
|
|
1917
|
+
}
|
|
1918
|
+
if (method === "thread/tokenUsage/updated") {
|
|
1919
|
+
const totalTokens = observeTurnId(params.turnId) &&
|
|
1920
|
+
isRecord(params.tokenUsage) &&
|
|
1921
|
+
isRecord(params.tokenUsage.last)
|
|
1922
|
+
? params.tokenUsage.last.totalTokens
|
|
1923
|
+
: null;
|
|
1924
|
+
if (!Number.isSafeInteger(totalTokens) ||
|
|
1925
|
+
totalTokens < 0 ||
|
|
1926
|
+
totalTokens > MAX_PROBE_TOKEN_COUNT) {
|
|
1927
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1928
|
+
}
|
|
1929
|
+
else {
|
|
1930
|
+
runtime.tokenCount = totalTokens;
|
|
1931
|
+
this.settleProbeRuntime();
|
|
1932
|
+
}
|
|
1933
|
+
return true;
|
|
1934
|
+
}
|
|
1935
|
+
if (method === "turn/started" || method === "turn/completed") {
|
|
1936
|
+
const turn = parseTurn(params.turn);
|
|
1937
|
+
if (turn === null ||
|
|
1938
|
+
!isRecord(params.turn) ||
|
|
1939
|
+
!Array.isArray(params.turn.items) ||
|
|
1940
|
+
!observeTurnId(turn.id)) {
|
|
1941
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1942
|
+
}
|
|
1943
|
+
else if (method === "turn/started") {
|
|
1944
|
+
if (turn.status !== "inProgress" ||
|
|
1945
|
+
runtime.phase !== "awaiting_start") {
|
|
1946
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1947
|
+
}
|
|
1948
|
+
else {
|
|
1949
|
+
runtime.phase = "started";
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
else if (turn.status === "completed" &&
|
|
1953
|
+
runtime.phase === "item_completed") {
|
|
1954
|
+
runtime.phase = "terminal";
|
|
1955
|
+
this.settleProbeRuntime();
|
|
1956
|
+
}
|
|
1957
|
+
else {
|
|
1958
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1959
|
+
}
|
|
1960
|
+
return true;
|
|
1961
|
+
}
|
|
1962
|
+
if (method === "item/started" || method === "item/completed") {
|
|
1963
|
+
const observedAt = method === "item/started" ? params.startedAtMs : params.completedAtMs;
|
|
1964
|
+
const itemType = isRecord(params.item) ? params.item.type : null;
|
|
1965
|
+
if (typeof itemType === "string" &&
|
|
1966
|
+
!PROBE_PASSIVE_ITEM_TYPES.has(itemType)) {
|
|
1967
|
+
fail("CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED");
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
if (!observeTurnId(params.turnId) ||
|
|
1971
|
+
!isRecord(params.item) ||
|
|
1972
|
+
typeof itemType !== "string" ||
|
|
1973
|
+
!Number.isSafeInteger(observedAt) ||
|
|
1974
|
+
observedAt < 0 ||
|
|
1975
|
+
(runtime.phase !== "started" && runtime.phase !== "item_completed")) {
|
|
1976
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1977
|
+
}
|
|
1978
|
+
else if (method === "item/completed" &&
|
|
1979
|
+
itemType === "agentMessage") {
|
|
1980
|
+
if (runtime.phase !== "started") {
|
|
1981
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1982
|
+
}
|
|
1983
|
+
else {
|
|
1984
|
+
runtime.phase = "item_completed";
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return true;
|
|
1988
|
+
}
|
|
1989
|
+
return false;
|
|
1990
|
+
}
|
|
1457
1991
|
handleCompletedItem(params) {
|
|
1458
1992
|
if (!isRecord(params) ||
|
|
1459
1993
|
typeof params.threadId !== "string") {
|
|
@@ -1597,6 +2131,7 @@ export class CodexAppServerConnector {
|
|
|
1597
2131
|
return;
|
|
1598
2132
|
this.faulting = true;
|
|
1599
2133
|
this.connection = "faulted";
|
|
2134
|
+
this.settleProbeConnectionLoss();
|
|
1600
2135
|
this.initialized = false;
|
|
1601
2136
|
this.routeStatus = "stale";
|
|
1602
2137
|
this.selectedThreadObserved = false;
|
|
@@ -1617,6 +2152,7 @@ export class CodexAppServerConnector {
|
|
|
1617
2152
|
if (this.connection === "closed" || this.connection === "faulted")
|
|
1618
2153
|
return;
|
|
1619
2154
|
this.connection = "closed";
|
|
2155
|
+
this.settleProbeConnectionLoss();
|
|
1620
2156
|
this.initialized = false;
|
|
1621
2157
|
this.routeStatus = "stale";
|
|
1622
2158
|
this.selectedThreadObserved = false;
|