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.
Files changed (52) hide show
  1. package/CHANGELOG.md +18 -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 +42 -1
  10. package/dist/src/gateway/codex-app-server.js +536 -2
  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 +12 -2
  35. package/dist/src/gateway/providers.js +326 -6
  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 +116 -37
  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);
@@ -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.5.0",
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: [...OUTPUT_NOTIFICATION_OPT_OUTS],
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;