agent-embassy 1.5.0 → 1.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/CHANGELOG.md +18 -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 +42 -1
- package/dist/src/gateway/codex-app-server.js +536 -2
- 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 +12 -2
- package/dist/src/gateway/providers.js +326 -6
- 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 +116 -37
- 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);
|
|
@@ -395,11 +479,12 @@ export class CodexAppServerConnector {
|
|
|
395
479
|
throw new CodexConnectorError("INVALID_CONFIGURATION");
|
|
396
480
|
}
|
|
397
481
|
this.writesEnabled = options.writesEnabled;
|
|
482
|
+
this.writeCompatibilityProbe = options.writeCompatibilityProbe === true;
|
|
398
483
|
this.transport = options.transport;
|
|
399
484
|
this.clientInfo = validateClientInfo(options.clientInfo ?? {
|
|
400
485
|
name: "agent_embassy_gateway",
|
|
401
486
|
title: "Embassy Gateway",
|
|
402
|
-
version: "1.
|
|
487
|
+
version: "1.6.0",
|
|
403
488
|
});
|
|
404
489
|
this.maxFrameBytes = positiveInteger(options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES, "INVALID_CONFIGURATION");
|
|
405
490
|
this.maxDeadlineMs = positiveInteger(options.maxDeadlineMs ?? DEFAULT_MAX_DEADLINE_MS, "INVALID_CONFIGURATION");
|
|
@@ -709,6 +794,309 @@ export class CodexAppServerConnector {
|
|
|
709
794
|
this.handleDisconnect();
|
|
710
795
|
}
|
|
711
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* Run the one fixed disposable-thread write probe. The method never exposes
|
|
799
|
+
* JSON-RPC, native IDs, content, or route state, and every failure settles as
|
|
800
|
+
* a safe result so compatibility discovery cannot take down the broker.
|
|
801
|
+
*/
|
|
802
|
+
async runWriteCompatibilityProbe(input) {
|
|
803
|
+
if (this.probeAttempted) {
|
|
804
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
805
|
+
}
|
|
806
|
+
this.probeAttempted = true;
|
|
807
|
+
let threadId = null;
|
|
808
|
+
let archivedThreadCount;
|
|
809
|
+
let result = codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
810
|
+
try {
|
|
811
|
+
if (!this.writeCompatibilityProbe ||
|
|
812
|
+
this.connection !== "ready" ||
|
|
813
|
+
this.inFlightMethod !== null ||
|
|
814
|
+
this.pending.size !== 0 ||
|
|
815
|
+
!isAbsolute(input.cwd) ||
|
|
816
|
+
!Array.isArray(input.forbiddenThreadIds) ||
|
|
817
|
+
!input.forbiddenThreadIds.every((value) => typeof value === "string" && validOpaqueId(value))) {
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
const beforeStat = await stat(input.cwd, { bigint: true });
|
|
821
|
+
const beforeEntries = await readdir(input.cwd);
|
|
822
|
+
if (!beforeStat.isDirectory() ||
|
|
823
|
+
beforeStat.uid !== BigInt(process.getuid?.() ?? -1) ||
|
|
824
|
+
(beforeStat.mode & 511n) !== 448n ||
|
|
825
|
+
beforeEntries.length !== 0) {
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
const limits = rateLimitConstrained(await this.request("account/rateLimits/read", null));
|
|
829
|
+
if (limits === null)
|
|
830
|
+
return result;
|
|
831
|
+
if (limits) {
|
|
832
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_RATE_LIMIT_CONSTRAINED");
|
|
833
|
+
}
|
|
834
|
+
const selection = this.selectProbeModel(await this.request("model/list", {
|
|
835
|
+
includeHidden: true,
|
|
836
|
+
limit: 100,
|
|
837
|
+
}).catch(() => null));
|
|
838
|
+
if (selection === null) {
|
|
839
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_MODEL_PIN_UNAVAILABLE");
|
|
840
|
+
}
|
|
841
|
+
const [model, effort] = selection;
|
|
842
|
+
const loadedBefore = parseLoadedThreadIds(await this.request("thread/loaded/list", {}));
|
|
843
|
+
if (loadedBefore === null)
|
|
844
|
+
return result;
|
|
845
|
+
const runtime = {
|
|
846
|
+
expectedCwd: input.cwd,
|
|
847
|
+
expectedEffort: effort,
|
|
848
|
+
expectedModel: model,
|
|
849
|
+
failCode: null,
|
|
850
|
+
phase: "awaiting_start",
|
|
851
|
+
resolveTurn: null,
|
|
852
|
+
settingsEchoObserved: false,
|
|
853
|
+
settingsThreadId: null,
|
|
854
|
+
threadId: null,
|
|
855
|
+
tokenCount: null,
|
|
856
|
+
turnId: null,
|
|
857
|
+
};
|
|
858
|
+
this.probeRuntime = runtime;
|
|
859
|
+
const started = await this.request("thread/start", {
|
|
860
|
+
allowProviderModelFallback: false,
|
|
861
|
+
approvalPolicy: "never",
|
|
862
|
+
cwd: input.cwd,
|
|
863
|
+
dynamicTools: [],
|
|
864
|
+
environments: [],
|
|
865
|
+
ephemeral: false,
|
|
866
|
+
model,
|
|
867
|
+
runtimeWorkspaceRoots: [],
|
|
868
|
+
sandbox: "read-only",
|
|
869
|
+
selectedCapabilityRoots: [],
|
|
870
|
+
});
|
|
871
|
+
const candidateThreadId = this.probeCleanupThreadId(started, loadedBefore, input.forbiddenThreadIds);
|
|
872
|
+
if (candidateThreadId === null ||
|
|
873
|
+
!this.validateProbeThread(started, input.cwd, model, candidateThreadId)) {
|
|
874
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
|
|
875
|
+
}
|
|
876
|
+
threadId = candidateThreadId;
|
|
877
|
+
runtime.threadId = candidateThreadId;
|
|
878
|
+
if (runtime.settingsThreadId !== null &&
|
|
879
|
+
runtime.settingsThreadId !== candidateThreadId) {
|
|
880
|
+
runtime.failCode = "CODEX_WRITE_PROBE_THREAD_SETUP_FAILED";
|
|
881
|
+
}
|
|
882
|
+
if (runtime.failCode === null) {
|
|
883
|
+
const turnResult = await this.request("turn/start", {
|
|
884
|
+
approvalPolicy: "never",
|
|
885
|
+
cwd: input.cwd,
|
|
886
|
+
effort,
|
|
887
|
+
environments: [],
|
|
888
|
+
input: [{ text: CODEX_WRITE_PROBE_INPUT, type: "text" }],
|
|
889
|
+
model,
|
|
890
|
+
runtimeWorkspaceRoots: [],
|
|
891
|
+
sandboxPolicy: { networkAccess: false, type: "readOnly" },
|
|
892
|
+
threadId,
|
|
893
|
+
});
|
|
894
|
+
const turn = isRecord(turnResult) ? parseTurn(turnResult.turn) : null;
|
|
895
|
+
if (turn === null ||
|
|
896
|
+
!isRecord(turnResult) ||
|
|
897
|
+
!isRecord(turnResult.turn) ||
|
|
898
|
+
!Array.isArray(turnResult.turn.items) ||
|
|
899
|
+
(turn.status !== "inProgress" && turn.status !== "completed")) {
|
|
900
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
|
|
901
|
+
}
|
|
902
|
+
if (runtime.turnId !== null && runtime.turnId !== turn.id) {
|
|
903
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
|
|
904
|
+
}
|
|
905
|
+
runtime.turnId = turn.id;
|
|
906
|
+
await this.waitForProbeTurn(runtime);
|
|
907
|
+
}
|
|
908
|
+
if (runtime.failCode !== null) {
|
|
909
|
+
result = codexWriteProbeFailure(runtime.failCode, {
|
|
910
|
+
settingsEchoObserved: runtime.settingsEchoObserved,
|
|
911
|
+
...(runtime.tokenCount === null
|
|
912
|
+
? {}
|
|
913
|
+
: { tokenCount: runtime.tokenCount }),
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
else if (runtime.phase === "terminal" &&
|
|
917
|
+
runtime.tokenCount !== null) {
|
|
918
|
+
const afterStat = await stat(input.cwd, { bigint: true });
|
|
919
|
+
const afterEntries = await readdir(input.cwd);
|
|
920
|
+
if (afterEntries.length === 0 &&
|
|
921
|
+
afterStat.dev === beforeStat.dev &&
|
|
922
|
+
afterStat.ino === beforeStat.ino &&
|
|
923
|
+
afterStat.mtimeNs === beforeStat.mtimeNs &&
|
|
924
|
+
afterStat.uid === beforeStat.uid &&
|
|
925
|
+
afterStat.mode === beforeStat.mode) {
|
|
926
|
+
result = {
|
|
927
|
+
archivedThreadCount: 1,
|
|
928
|
+
outcome: "pass",
|
|
929
|
+
settingsEchoObserved: runtime.settingsEchoObserved,
|
|
930
|
+
tokenCount: runtime.tokenCount,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
result = codexWriteProbeFailure("CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED", {
|
|
935
|
+
settingsEchoObserved: runtime.settingsEchoObserved,
|
|
936
|
+
tokenCount: runtime.tokenCount,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
const normalized = this.normalizeError(error, true);
|
|
943
|
+
const runtime = this.probeRuntime;
|
|
944
|
+
result = codexWriteProbeFailure(normalized.code === "REQUEST_TIMEOUT"
|
|
945
|
+
? "CODEX_WRITE_PROBE_TIMEOUT"
|
|
946
|
+
: "CODEX_WRITE_PROBE_THREAD_SETUP_FAILED", {
|
|
947
|
+
settingsEchoObserved: runtime?.settingsEchoObserved ?? false,
|
|
948
|
+
...(runtime?.tokenCount === null || runtime?.tokenCount === undefined
|
|
949
|
+
? {}
|
|
950
|
+
: { tokenCount: runtime.tokenCount }),
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
const runtime = this.probeRuntime;
|
|
955
|
+
if (threadId !== null &&
|
|
956
|
+
runtime !== null &&
|
|
957
|
+
runtime.turnId !== null &&
|
|
958
|
+
(result.outcome === "fail" || runtime.failCode !== null)) {
|
|
959
|
+
try {
|
|
960
|
+
await this.request("turn/interrupt", {
|
|
961
|
+
threadId,
|
|
962
|
+
turnId: runtime.turnId,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
// Archival remains the one required cleanup proof below.
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
let cleanupUnconfirmed = runtime !== null && threadId === null;
|
|
970
|
+
if (threadId !== null) {
|
|
971
|
+
try {
|
|
972
|
+
const archived = await this.request("thread/archive", { threadId });
|
|
973
|
+
if (!isRecord(archived) || Object.keys(archived).length !== 0) {
|
|
974
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
|
|
975
|
+
}
|
|
976
|
+
archivedThreadCount = 1;
|
|
977
|
+
const unsubscribed = await this.request("thread/unsubscribe", {
|
|
978
|
+
threadId,
|
|
979
|
+
});
|
|
980
|
+
if (!isRecord(unsubscribed) ||
|
|
981
|
+
(unsubscribed.status !== "unsubscribed" &&
|
|
982
|
+
unsubscribed.status !== "notSubscribed" &&
|
|
983
|
+
unsubscribed.status !== "notLoaded")) {
|
|
984
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
|
|
985
|
+
}
|
|
986
|
+
const loadedAfter = parseLoadedThreadIds(await this.request("thread/loaded/list", {}));
|
|
987
|
+
if (loadedAfter === null || loadedAfter.includes(threadId)) {
|
|
988
|
+
throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
cleanupUnconfirmed = true;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const tokenCount = runtime?.tokenCount ?? result.tokenCount;
|
|
996
|
+
const evidence = {
|
|
997
|
+
...(archivedThreadCount === undefined ? {} : { archivedThreadCount }),
|
|
998
|
+
settingsEchoObserved: runtime?.settingsEchoObserved ?? result.settingsEchoObserved,
|
|
999
|
+
...(tokenCount === undefined ? {} : { tokenCount }),
|
|
1000
|
+
};
|
|
1001
|
+
if (cleanupUnconfirmed || (result.outcome === "pass" && archivedThreadCount !== 1)) {
|
|
1002
|
+
result = codexWriteProbeFailure("CODEX_WRITE_PROBE_CLEANUP_UNCONFIRMED", evidence);
|
|
1003
|
+
}
|
|
1004
|
+
else if (runtime?.failCode !== null && runtime?.failCode !== undefined) {
|
|
1005
|
+
result = codexWriteProbeFailure(runtime.failCode, evidence);
|
|
1006
|
+
}
|
|
1007
|
+
if (result.outcome === "fail") {
|
|
1008
|
+
result = codexWriteProbeFailure(result.safeErrorCode, evidence);
|
|
1009
|
+
}
|
|
1010
|
+
this.probeRuntime = null;
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
selectProbeModel(value) {
|
|
1015
|
+
if (!isRecord(value) || !Array.isArray(value.data) || value.data.length > 100) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
const data = value.data;
|
|
1019
|
+
for (const model of CODEX_PROBE_MODEL_PREFERENCE) {
|
|
1020
|
+
const candidate = data.find((entry) => isRecord(entry) &&
|
|
1021
|
+
entry.model === model &&
|
|
1022
|
+
entry.hidden === false);
|
|
1023
|
+
if (!isRecord(candidate) || !Array.isArray(candidate.supportedReasoningEfforts)) {
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const supported = candidate.supportedReasoningEfforts;
|
|
1027
|
+
const effort = CODEX_PROBE_EFFORT.find((preference) => supported.some((option) => isRecord(option) && option.reasoningEffort === preference));
|
|
1028
|
+
if (effort !== undefined)
|
|
1029
|
+
return [model, effort];
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
probeCleanupThreadId(value, loadedBefore, forbiddenThreadIds) {
|
|
1034
|
+
if (!isRecord(value) ||
|
|
1035
|
+
!isRecord(value.thread) ||
|
|
1036
|
+
typeof value.thread.id !== "string" ||
|
|
1037
|
+
!validUuidV7(value.thread.id) ||
|
|
1038
|
+
value.thread.id === this.route.threadId ||
|
|
1039
|
+
loadedBefore.includes(value.thread.id) ||
|
|
1040
|
+
forbiddenThreadIds.includes(value.thread.id)) {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
return value.thread.id;
|
|
1044
|
+
}
|
|
1045
|
+
validateProbeThread(value, cwd, model, threadId) {
|
|
1046
|
+
if (!isRecord(value) || !isRecord(value.thread))
|
|
1047
|
+
return false;
|
|
1048
|
+
const roots = value.runtimeWorkspaceRoots;
|
|
1049
|
+
const networkAccess = isRecord(value.sandbox)
|
|
1050
|
+
? value.sandbox.networkAccess
|
|
1051
|
+
: undefined;
|
|
1052
|
+
return (value.approvalPolicy === "never" &&
|
|
1053
|
+
value.cwd === cwd &&
|
|
1054
|
+
value.model === model &&
|
|
1055
|
+
isRecord(value.sandbox) &&
|
|
1056
|
+
value.sandbox.type === "readOnly" &&
|
|
1057
|
+
(networkAccess === undefined || networkAccess === false) &&
|
|
1058
|
+
(roots === undefined || (Array.isArray(roots) && roots.length === 0)) &&
|
|
1059
|
+
value.thread.id === threadId &&
|
|
1060
|
+
value.thread.cwd === cwd &&
|
|
1061
|
+
value.thread.ephemeral === false &&
|
|
1062
|
+
isRecord(value.thread.status) &&
|
|
1063
|
+
value.thread.status.type === "idle" &&
|
|
1064
|
+
Array.isArray(value.thread.turns) &&
|
|
1065
|
+
value.thread.turns.length === 0);
|
|
1066
|
+
}
|
|
1067
|
+
async waitForProbeTurn(runtime) {
|
|
1068
|
+
if (runtime.failCode !== null ||
|
|
1069
|
+
(runtime.phase === "terminal" && runtime.tokenCount !== null)) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
let timer;
|
|
1073
|
+
await new Promise((resolve) => {
|
|
1074
|
+
runtime.resolveTurn = resolve;
|
|
1075
|
+
timer = setTimeout(resolve, this.turnWatchdogMs);
|
|
1076
|
+
});
|
|
1077
|
+
runtime.resolveTurn = null;
|
|
1078
|
+
if (timer !== undefined)
|
|
1079
|
+
clearTimeout(timer);
|
|
1080
|
+
if (runtime.failCode === null &&
|
|
1081
|
+
!(runtime.phase === "terminal" && runtime.tokenCount !== null)) {
|
|
1082
|
+
runtime.failCode = "CODEX_WRITE_PROBE_TIMEOUT";
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
settleProbeRuntime() {
|
|
1086
|
+
const runtime = this.probeRuntime;
|
|
1087
|
+
if (runtime === null || runtime.resolveTurn === null)
|
|
1088
|
+
return;
|
|
1089
|
+
if (runtime.failCode !== null ||
|
|
1090
|
+
(runtime.phase === "terminal" && runtime.tokenCount !== null)) {
|
|
1091
|
+
runtime.resolveTurn();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
settleProbeConnectionLoss() {
|
|
1095
|
+
if (this.probeRuntime === null)
|
|
1096
|
+
return;
|
|
1097
|
+
this.probeRuntime.failCode ??= "CODEX_WRITE_PROBE_THREAD_SETUP_FAILED";
|
|
1098
|
+
this.settleProbeRuntime();
|
|
1099
|
+
}
|
|
712
1100
|
async initialize() {
|
|
713
1101
|
try {
|
|
714
1102
|
const result = await this.request("initialize", {
|
|
@@ -717,7 +1105,7 @@ export class CodexAppServerConnector {
|
|
|
717
1105
|
// in the exactly attested 0.147.0 schema. The client method allowlist
|
|
718
1106
|
// remains closed and exposes no experimental method.
|
|
719
1107
|
experimentalApi: true,
|
|
720
|
-
optOutNotificationMethods:
|
|
1108
|
+
optOutNotificationMethods: OUTPUT_NOTIFICATION_OPT_OUTS.filter((method) => method !== "item/started" || !this.writeCompatibilityProbe),
|
|
721
1109
|
},
|
|
722
1110
|
clientInfo: this.clientInfo,
|
|
723
1111
|
});
|
|
@@ -1085,6 +1473,11 @@ export class CodexAppServerConnector {
|
|
|
1085
1473
|
if (method !== "initialize" && !V1_METHOD_SET.has(method)) {
|
|
1086
1474
|
return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
|
|
1087
1475
|
}
|
|
1476
|
+
if (method !== "initialize" &&
|
|
1477
|
+
PROBE_ONLY_METHODS.has(method) &&
|
|
1478
|
+
!this.writeCompatibilityProbe) {
|
|
1479
|
+
return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
|
|
1480
|
+
}
|
|
1088
1481
|
if (method === "initialize") {
|
|
1089
1482
|
if (this.initializeRequested || this.connection !== "connecting") {
|
|
1090
1483
|
return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
|
|
@@ -1201,6 +1594,8 @@ export class CodexAppServerConnector {
|
|
|
1201
1594
|
pending.resolve(parsed.result);
|
|
1202
1595
|
}
|
|
1203
1596
|
handleNotification(method, params) {
|
|
1597
|
+
if (this.handleProbeNotification(method, params))
|
|
1598
|
+
return;
|
|
1204
1599
|
if (method === "item/completed") {
|
|
1205
1600
|
this.handleCompletedItem(params);
|
|
1206
1601
|
return;
|
|
@@ -1430,6 +1825,14 @@ export class CodexAppServerConnector {
|
|
|
1430
1825
|
// item/model/tool payloads are neither normalized nor retained.
|
|
1431
1826
|
}
|
|
1432
1827
|
handleServerRequest(method, params) {
|
|
1828
|
+
if (this.probeRuntime !== null &&
|
|
1829
|
+
isRecord(params) &&
|
|
1830
|
+
params.threadId === this.probeRuntime.threadId) {
|
|
1831
|
+
this.probeRuntime.failCode ??=
|
|
1832
|
+
"CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED";
|
|
1833
|
+
this.settleProbeRuntime();
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1433
1836
|
if (!APPROVAL_REQUEST_METHODS.has(method)) {
|
|
1434
1837
|
this.emit("server_request_ignored");
|
|
1435
1838
|
return;
|
|
@@ -1454,6 +1857,135 @@ export class CodexAppServerConnector {
|
|
|
1454
1857
|
// Intentionally no JSON-RPC response: the gateway is not an approval UI or
|
|
1455
1858
|
// authority. The owning Desktop client must resolve the request.
|
|
1456
1859
|
}
|
|
1860
|
+
handleProbeNotification(method, params) {
|
|
1861
|
+
const runtime = this.probeRuntime;
|
|
1862
|
+
if (runtime === null)
|
|
1863
|
+
return false;
|
|
1864
|
+
const fail = (code) => {
|
|
1865
|
+
runtime.failCode ??= code;
|
|
1866
|
+
this.settleProbeRuntime();
|
|
1867
|
+
};
|
|
1868
|
+
if (method === "model/rerouted") {
|
|
1869
|
+
fail("CODEX_WRITE_PROBE_MODEL_REROUTED");
|
|
1870
|
+
return true;
|
|
1871
|
+
}
|
|
1872
|
+
if (!isRecord(params))
|
|
1873
|
+
return false;
|
|
1874
|
+
const observeTurnId = (value) => {
|
|
1875
|
+
if (typeof value !== "string" || !validOpaqueId(value))
|
|
1876
|
+
return false;
|
|
1877
|
+
if (runtime.turnId !== null && runtime.turnId !== value)
|
|
1878
|
+
return false;
|
|
1879
|
+
runtime.turnId = value;
|
|
1880
|
+
return true;
|
|
1881
|
+
};
|
|
1882
|
+
if (method === "thread/settings/updated") {
|
|
1883
|
+
runtime.settingsEchoObserved = true;
|
|
1884
|
+
const settingsThreadId = params.threadId;
|
|
1885
|
+
const settings = params.threadSettings;
|
|
1886
|
+
const sandbox = isRecord(settings) ? settings.sandboxPolicy : null;
|
|
1887
|
+
if (typeof settingsThreadId !== "string" ||
|
|
1888
|
+
!validUuidV7(settingsThreadId) ||
|
|
1889
|
+
(runtime.threadId !== null && runtime.threadId !== settingsThreadId) ||
|
|
1890
|
+
(runtime.settingsThreadId !== null &&
|
|
1891
|
+
runtime.settingsThreadId !== settingsThreadId) ||
|
|
1892
|
+
!isRecord(settings) ||
|
|
1893
|
+
settings.cwd !== runtime.expectedCwd ||
|
|
1894
|
+
settings.model !== runtime.expectedModel ||
|
|
1895
|
+
settings.approvalPolicy !== "never" ||
|
|
1896
|
+
(settings.effort !== undefined &&
|
|
1897
|
+
settings.effort !== runtime.expectedEffort) ||
|
|
1898
|
+
!isRecord(sandbox) ||
|
|
1899
|
+
sandbox.type !== "readOnly" ||
|
|
1900
|
+
(sandbox.networkAccess !== undefined &&
|
|
1901
|
+
sandbox.networkAccess !== false)) {
|
|
1902
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
runtime.settingsThreadId = settingsThreadId;
|
|
1906
|
+
}
|
|
1907
|
+
return true;
|
|
1908
|
+
}
|
|
1909
|
+
if (runtime.threadId === null || params.threadId !== runtime.threadId) {
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
if (PROBE_TOOL_NOTIFICATION_METHODS.has(method)) {
|
|
1913
|
+
fail("CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED");
|
|
1914
|
+
return true;
|
|
1915
|
+
}
|
|
1916
|
+
if (method === "thread/tokenUsage/updated") {
|
|
1917
|
+
const totalTokens = observeTurnId(params.turnId) &&
|
|
1918
|
+
isRecord(params.tokenUsage) &&
|
|
1919
|
+
isRecord(params.tokenUsage.last)
|
|
1920
|
+
? params.tokenUsage.last.totalTokens
|
|
1921
|
+
: null;
|
|
1922
|
+
if (!Number.isSafeInteger(totalTokens) ||
|
|
1923
|
+
totalTokens < 0 ||
|
|
1924
|
+
totalTokens > MAX_PROBE_TOKEN_COUNT) {
|
|
1925
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1926
|
+
}
|
|
1927
|
+
else {
|
|
1928
|
+
runtime.tokenCount = totalTokens;
|
|
1929
|
+
this.settleProbeRuntime();
|
|
1930
|
+
}
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
if (method === "turn/started" || method === "turn/completed") {
|
|
1934
|
+
const turn = parseTurn(params.turn);
|
|
1935
|
+
if (turn === null ||
|
|
1936
|
+
!isRecord(params.turn) ||
|
|
1937
|
+
!Array.isArray(params.turn.items) ||
|
|
1938
|
+
!observeTurnId(turn.id)) {
|
|
1939
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1940
|
+
}
|
|
1941
|
+
else if (method === "turn/started") {
|
|
1942
|
+
if (turn.status !== "inProgress" ||
|
|
1943
|
+
runtime.phase !== "awaiting_start") {
|
|
1944
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
runtime.phase = "started";
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
else if (turn.status === "completed" &&
|
|
1951
|
+
runtime.phase === "item_completed") {
|
|
1952
|
+
runtime.phase = "terminal";
|
|
1953
|
+
this.settleProbeRuntime();
|
|
1954
|
+
}
|
|
1955
|
+
else {
|
|
1956
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1957
|
+
}
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
if (method === "item/started" || method === "item/completed") {
|
|
1961
|
+
const observedAt = method === "item/started" ? params.startedAtMs : params.completedAtMs;
|
|
1962
|
+
const itemType = isRecord(params.item) ? params.item.type : null;
|
|
1963
|
+
if (typeof itemType === "string" &&
|
|
1964
|
+
!PROBE_PASSIVE_ITEM_TYPES.has(itemType)) {
|
|
1965
|
+
fail("CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED");
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
if (!observeTurnId(params.turnId) ||
|
|
1969
|
+
!isRecord(params.item) ||
|
|
1970
|
+
typeof itemType !== "string" ||
|
|
1971
|
+
!Number.isSafeInteger(observedAt) ||
|
|
1972
|
+
observedAt < 0 ||
|
|
1973
|
+
(runtime.phase !== "started" && runtime.phase !== "item_completed")) {
|
|
1974
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1975
|
+
}
|
|
1976
|
+
else if (method === "item/completed" &&
|
|
1977
|
+
itemType === "agentMessage") {
|
|
1978
|
+
if (runtime.phase !== "started") {
|
|
1979
|
+
fail("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
1980
|
+
}
|
|
1981
|
+
else {
|
|
1982
|
+
runtime.phase = "item_completed";
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return true;
|
|
1986
|
+
}
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1457
1989
|
handleCompletedItem(params) {
|
|
1458
1990
|
if (!isRecord(params) ||
|
|
1459
1991
|
typeof params.threadId !== "string") {
|
|
@@ -1597,6 +2129,7 @@ export class CodexAppServerConnector {
|
|
|
1597
2129
|
return;
|
|
1598
2130
|
this.faulting = true;
|
|
1599
2131
|
this.connection = "faulted";
|
|
2132
|
+
this.settleProbeConnectionLoss();
|
|
1600
2133
|
this.initialized = false;
|
|
1601
2134
|
this.routeStatus = "stale";
|
|
1602
2135
|
this.selectedThreadObserved = false;
|
|
@@ -1617,6 +2150,7 @@ export class CodexAppServerConnector {
|
|
|
1617
2150
|
if (this.connection === "closed" || this.connection === "faulted")
|
|
1618
2151
|
return;
|
|
1619
2152
|
this.connection = "closed";
|
|
2153
|
+
this.settleProbeConnectionLoss();
|
|
1620
2154
|
this.initialized = false;
|
|
1621
2155
|
this.routeStatus = "stale";
|
|
1622
2156
|
this.selectedThreadObserved = false;
|