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.
Files changed (52) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/CONTRIBUTING.md +11 -3
  3. package/README.md +4 -2
  4. package/README.zh-CN.md +4 -2
  5. package/SECURITY.md +30 -9
  6. package/dist/src/gateway/cli.d.ts +1 -1
  7. package/dist/src/gateway/cli.js +4 -1
  8. package/dist/src/gateway/cli.js.map +1 -1
  9. package/dist/src/gateway/codex-app-server.d.ts +43 -2
  10. package/dist/src/gateway/codex-app-server.js +539 -3
  11. package/dist/src/gateway/codex-app-server.js.map +1 -1
  12. package/dist/src/gateway/compatibility.d.ts +31 -3
  13. package/dist/src/gateway/compatibility.js +100 -29
  14. package/dist/src/gateway/compatibility.js.map +1 -1
  15. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  16. package/dist/src/gateway/dashboard-copy.en.d.ts +6 -0
  17. package/dist/src/gateway/dashboard-copy.en.js +6 -0
  18. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  19. package/dist/src/gateway/dashboard-copy.js +6 -0
  20. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  21. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +6 -0
  22. package/dist/src/gateway/dashboard-copy.zh-CN.js +6 -0
  23. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  24. package/dist/src/gateway/dashboard-model.d.ts +27 -7
  25. package/dist/src/gateway/dashboard-model.js +78 -25
  26. package/dist/src/gateway/dashboard-model.js.map +1 -1
  27. package/dist/src/gateway/dashboard.d.ts +3 -0
  28. package/dist/src/gateway/dashboard.js +15 -3
  29. package/dist/src/gateway/dashboard.js.map +1 -1
  30. package/dist/src/gateway/deepseek-detect.d.ts +13 -0
  31. package/dist/src/gateway/deepseek-detect.js +125 -0
  32. package/dist/src/gateway/deepseek-detect.js.map +1 -0
  33. package/dist/src/gateway/live-dashboard-app/app.js +12 -11
  34. package/dist/src/gateway/providers.d.ts +13 -2
  35. package/dist/src/gateway/providers.js +370 -24
  36. package/dist/src/gateway/providers.js.map +1 -1
  37. package/dist/src/gateway/server.d.ts +2 -0
  38. package/dist/src/gateway/server.js +6 -0
  39. package/dist/src/gateway/server.js.map +1 -1
  40. package/dist/src/gateway/service.d.ts +21 -2
  41. package/dist/src/gateway/service.js +122 -38
  42. package/dist/src/gateway/service.js.map +1 -1
  43. package/dist/src/gateway/types.d.ts +4 -3
  44. package/dist/src/gateway/types.js +17 -14
  45. package/dist/src/gateway/types.js.map +1 -1
  46. package/docs/CONFIGURATION.md +2 -2
  47. package/docs/CONFIGURATION.zh-CN.md +2 -2
  48. package/docs/DASHBOARD.md +10 -4
  49. package/docs/DASHBOARD.zh-CN.md +1 -1
  50. package/docs/GATEWAY-ARCHITECTURE.md +40 -14
  51. package/package.json +1 -1
  52. 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.5.0",
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: [...OUTPUT_NOTIFICATION_OPT_OUTS],
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;