dsh-codex-connect 0.1.0-alpha.4.19 → 0.1.0-alpha.4.21

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.
@@ -1,4 +1,4 @@
1
- import { randomBytes, randomUUID } from "node:crypto";
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { basename, dirname, join, parse, resolve } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { deepEqualJson, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
@@ -9,7 +9,10 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
9
9
  import { link, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
10
10
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
11
11
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
12
+ import { AsyncLocalStorage } from "node:async_hooks";
13
+ import { Dispatcher, ProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
12
14
  import { fileURLToPath } from "node:url";
15
+ import { SessionId } from "@deepseek-ai/dsh-session";
13
16
  import { AttachmentId } from "@deepseek-ai/dsh-attachment";
14
17
  import { TOOL_ABORTED, defineTool } from "@deepseek-ai/dsh-tools";
15
18
  import { lookup } from "node:dns/promises";
@@ -17,8 +20,8 @@ import { request } from "node:http";
17
20
  import { request as request$1 } from "node:https";
18
21
  import { BlockList, isIP } from "node:net";
19
22
  import { Service } from "@deepseek-ai/cordis";
20
- import { WebError } from "@deepseek-ai/dsh-web";
21
23
  import { constants } from "node:fs";
24
+ import { WebError } from "@deepseek-ai/dsh-web";
22
25
  import { constants as constants$1, zstdCompressSync, zstdDecompressSync } from "node:zlib";
23
26
  //#region src/store.ts
24
27
  /**
@@ -51,7 +54,7 @@ async function assertOwnerOnly$1(filename) {
51
54
  /* v8 ignore stop */
52
55
  }
53
56
  /** Validate the strict JSON document without quoting token-bearing input. */
54
- function parseDocument$1(text, filename) {
57
+ function parseDocument$2(text, filename) {
55
58
  let value;
56
59
  try {
57
60
  value = JSON.parse(text);
@@ -116,7 +119,7 @@ var OpenAICodexCredentialStore = class {
116
119
  if (isENOENT$1(error)) return void 0;
117
120
  throw error;
118
121
  }
119
- return cloneCredential(parseDocument$1(text, this.filename).credential);
122
+ return cloneCredential(parseDocument$2(text, this.filename).credential);
120
123
  }
121
124
  /** @inheritdoc */
122
125
  async read(providerId) {
@@ -140,7 +143,7 @@ var OpenAICodexCredentialStore = class {
140
143
  const current = await this.readCurrent();
141
144
  const candidate = await fn(current);
142
145
  if (candidate === void 0) return current;
143
- const document = parseDocument$1(JSON.stringify({
146
+ const document = parseDocument$2(JSON.stringify({
144
147
  version: AUTH_FORMAT_VERSION,
145
148
  credential: candidate
146
149
  }), this.filename);
@@ -212,9 +215,16 @@ function withOpenAICodexFastMode(provider, fastMode) {
212
215
  }
213
216
  };
214
217
  }
215
- function requestProvider(provider, fastMode) {
218
+ function requestProvider(provider, fastMode, proxyManager, resolveProxyUrl) {
219
+ const configured = withOpenAICodexFastMode(provider, fastMode);
220
+ const streamSimple = configured.streamSimple;
216
221
  return {
217
- ...withOpenAICodexFastMode(provider, fastMode),
222
+ ...configured,
223
+ streamSimple(model, context, options) {
224
+ const proxyUrl = resolveProxyUrl?.();
225
+ const operation = () => streamSimple.call(configured, model, context, options);
226
+ return proxyManager?.runStream(proxyUrl, operation) ?? operation();
227
+ },
218
228
  auth: {
219
229
  ...provider.auth,
220
230
  apiKey: {
@@ -231,17 +241,18 @@ function requestProvider(provider, fastMode) {
231
241
  };
232
242
  }
233
243
  /** Build the immutable profile consumed by the rc.2 pi-ai adapter. */
234
- function createOpenAICodexProfile(provider, fastMode) {
244
+ function createOpenAICodexProfile(provider, fastMode, proxyManager, resolveProxyUrl) {
235
245
  return {
236
246
  provider: OPENAI_CODEX_PROVIDER,
237
247
  displayName: "OpenAI Codex",
248
+ transport: "sse",
238
249
  streamIdleTimeoutMs: OPENAI_CODEX_STREAM_IDLE_TIMEOUT_MS,
239
250
  maxRequestImageBytes: OPENAI_CODEX_MAX_REQUEST_IMAGE_BYTES,
240
251
  requestImagePixelBudget: OPENAI_CODEX_REQUEST_IMAGE_PIXEL_BUDGET,
241
252
  requestImageMaxBytes: OPENAI_CODEX_REQUEST_IMAGE_MAX_BYTES,
242
253
  retryPolicy: resolveRetryPolicy(void 0, "dsh-codex-connect retryPolicy"),
243
254
  configuredMaxTokens: /* @__PURE__ */ new Map(),
244
- piProvider: requestProvider(provider, fastMode)
255
+ piProvider: requestProvider(provider, fastMode, proxyManager, resolveProxyUrl)
245
256
  };
246
257
  }
247
258
  /**
@@ -250,9 +261,9 @@ function createOpenAICodexProfile(provider, fastMode) {
250
261
  * resolution, streaming, reasoning metadata, and compaction behavior; this
251
262
  * plugin supplies its provider-native OAuth token for each request.
252
263
  */
253
- function createOpenAICodexAdapter(credentials, resolveAttachments, fastMode, visibleModelIds) {
264
+ function createOpenAICodexAdapter(credentials, resolveAttachments, fastMode, visibleModelIds, proxyManager, resolveProxyUrl) {
254
265
  const provider = openaiCodexProvider();
255
- const profiles = /* @__PURE__ */ new Map([[OPENAI_CODEX_PROVIDER, createOpenAICodexProfile(provider, fastMode)]]);
266
+ const profiles = /* @__PURE__ */ new Map([[OPENAI_CODEX_PROVIDER, createOpenAICodexProfile(provider, fastMode, proxyManager, resolveProxyUrl)]]);
256
267
  const models = createModels({ credentials });
257
268
  models.setProvider(provider);
258
269
  class OpenAICodexAdapter extends PiAiAdapter {
@@ -266,7 +277,10 @@ function createOpenAICodexAdapter(credentials, resolveAttachments, fastMode, vis
266
277
  }
267
278
  return new OpenAICodexAdapter({
268
279
  profiles: () => profiles,
269
- resolveApiKey: async () => (await models.getAuth(OPENAI_CODEX_PROVIDER))?.auth.apiKey,
280
+ resolveApiKey: async () => {
281
+ const operation = async () => (await models.getAuth(OPENAI_CODEX_PROVIDER))?.auth.apiKey;
282
+ return proxyManager?.run(resolveProxyUrl?.(), operation) ?? operation();
283
+ },
270
284
  auth: {
271
285
  credentials,
272
286
  authContext: defaultProviderAuthContext()
@@ -508,7 +522,7 @@ async function assertOwnerOnly(filename) {
508
522
  /* v8 ignore stop */
509
523
  }
510
524
  /** Reject malformed input without echoing its contents into an error. */
511
- function parseDocument(text, filename) {
525
+ function parseDocument$1(text, filename) {
512
526
  let value;
513
527
  try {
514
528
  value = JSON.parse(text);
@@ -588,7 +602,7 @@ var OpenAICodexTrustedOriginsStore = class {
588
602
  };
589
603
  throw error;
590
604
  }
591
- return parseDocument(text, this.filename);
605
+ return parseDocument$1(text, this.filename);
592
606
  }
593
607
  /** Read the current canonical list without acquiring the writer lock. */
594
608
  async list() {
@@ -750,9 +764,13 @@ var OpenAICodexWebAuth = class {
750
764
  challengeWaiters = [];
751
765
  challengeTimer;
752
766
  challengeTimeoutMs;
767
+ proxyManager;
768
+ resolveProxyUrl;
753
769
  constructor(store, options = {}) {
754
770
  this.store = store;
755
771
  this.challengeTimeoutMs = options.challengeTimeoutMs ?? 3e4;
772
+ this.proxyManager = options.proxyManager;
773
+ this.resolveProxyUrl = options.resolveProxyUrl ?? (() => void 0);
756
774
  if (!Number.isFinite(this.challengeTimeoutMs) || this.challengeTimeoutMs <= 0) throw new TypeError("OpenAI Codex auth URL timeout must be a positive finite number");
757
775
  }
758
776
  /** Read current public state, consulting durable storage while idle. */
@@ -794,13 +812,14 @@ var OpenAICodexWebAuth = class {
794
812
  this.cancelSignIn(/* @__PURE__ */ new Error(`OpenAI Codex did not provide an authorization URL within ${String(this.challengeTimeoutMs)}ms`));
795
813
  }, this.challengeTimeoutMs);
796
814
  this.challengeTimer.unref();
797
- this.operation = loginOpenAICodex({
815
+ const login = () => loginOpenAICodex({
798
816
  signal: cancellation.signal,
799
817
  prompt: (prompt) => prompt.type === "select" ? Promise.resolve("browser") : waitForPromptAbort(prompt),
800
818
  notify: (event) => {
801
819
  this.onEvent(event);
802
820
  }
803
- }, this.store).then(async () => {
821
+ }, this.store);
822
+ this.operation = (this.proxyManager?.run(this.resolveProxyUrl(), login) ?? login()).then(async () => {
804
823
  if (this.challenge === void 0) {
805
824
  const error = /* @__PURE__ */ new Error("OpenAI Codex sign-in finished without an authorization URL");
806
825
  this.rejectChallenge(error);
@@ -846,9 +865,10 @@ var OpenAICodexWebAuth = class {
846
865
  async readStoredStatus() {
847
866
  if (!(await openAICodexAuthStatus(this.store)).authenticated) return { status: "signed-out" };
848
867
  try {
868
+ const readUsage = () => readOpenAICodexRateLimits(this.store);
849
869
  return {
850
870
  status: "signed-in",
851
- usage: await readOpenAICodexRateLimits(this.store)
871
+ usage: await (this.proxyManager?.run(this.resolveProxyUrl(), readUsage) ?? readUsage())
852
872
  };
853
873
  } catch (error) {
854
874
  if (isOpenAICodexReauthRequiredError(error)) return {
@@ -944,7 +964,7 @@ async function trustedRequestDecision(req, trustedOrigins = new OpenAICodexTrust
944
964
  error: REMOTE_WEB_ORIGIN_NOT_TRUSTED
945
965
  };
946
966
  }
947
- function json$2(res, status, value) {
967
+ function json$4(res, status, value) {
948
968
  res.writeHead(status, {
949
969
  "content-type": "application/json; charset=utf-8",
950
970
  "cache-control": "no-store",
@@ -1025,8 +1045,11 @@ function fastModeBody(value) {
1025
1045
  } : void 0;
1026
1046
  }
1027
1047
  /** Register the plugin-owned OAuth routes when the Web server is composed. */
1028
- function registerOpenAICodexAuthRoutes(ctx, store, trustedOriginsOverride, fastModeOverride) {
1029
- const auth = new OpenAICodexWebAuth(store);
1048
+ function registerOpenAICodexAuthRoutes(ctx, store, trustedOriginsOverride, fastModeOverride, proxyManager, resolveProxyUrl) {
1049
+ const auth = new OpenAICodexWebAuth(store, {
1050
+ proxyManager,
1051
+ resolveProxyUrl
1052
+ });
1030
1053
  const storedFilename = store.filename;
1031
1054
  const fastMode = fastModeOverride ?? new FastModeRegistry();
1032
1055
  const trustedOrigins = trustedOriginsOverride ?? (typeof storedFilename === "string" ? new OpenAICodexTrustedOriginsStore(join(dirname(storedFilename), ".openai-codex-trusted-origins.json")) : new OpenAICodexTrustedOriginsStore());
@@ -1034,7 +1057,7 @@ function registerOpenAICodexAuthRoutes(ctx, store, trustedOriginsOverride, fastM
1034
1057
  const authorize = async (req, res) => {
1035
1058
  const decision = await trustedRequestDecision(req, trustedOrigins);
1036
1059
  if (decision.trusted) return true;
1037
- json$2(res, 403, { error: decision.error });
1060
+ json$4(res, 403, { error: decision.error });
1038
1061
  return false;
1039
1062
  };
1040
1063
  const routes = [
@@ -1042,21 +1065,21 @@ function registerOpenAICodexAuthRoutes(ctx, store, trustedOriginsOverride, fastM
1042
1065
  kind: "exact",
1043
1066
  path: OPENAI_CODEX_AUTH_STATUS_PATH,
1044
1067
  handler: async (req, res) => {
1045
- if (req.method !== "GET") return json$2(res, 405, { error: "method not allowed" });
1068
+ if (req.method !== "GET") return json$4(res, 405, { error: "method not allowed" });
1046
1069
  if (!await authorize(req, res)) return;
1047
- json$2(res, 200, await auth.status());
1070
+ json$4(res, 200, await auth.status());
1048
1071
  }
1049
1072
  }),
1050
1073
  ctx.webServer.register({
1051
1074
  kind: "exact",
1052
1075
  path: OPENAI_CODEX_AUTH_LOGIN_PATH,
1053
1076
  handler: async (req, res) => {
1054
- if (req.method !== "POST") return json$2(res, 405, { error: "method not allowed" });
1077
+ if (req.method !== "POST") return json$4(res, 405, { error: "method not allowed" });
1055
1078
  if (!await authorize(req, res)) return;
1056
1079
  try {
1057
- json$2(res, 200, await auth.signIn());
1080
+ json$4(res, 200, await auth.signIn());
1058
1081
  } catch (error) {
1059
- json$2(res, 500, { error: safeMessage(error) });
1082
+ json$4(res, 500, { error: safeMessage(error) });
1060
1083
  }
1061
1084
  }
1062
1085
  }),
@@ -1064,13 +1087,13 @@ function registerOpenAICodexAuthRoutes(ctx, store, trustedOriginsOverride, fastM
1064
1087
  kind: "exact",
1065
1088
  path: OPENAI_CODEX_AUTH_LOGOUT_PATH,
1066
1089
  handler: async (req, res) => {
1067
- if (req.method !== "POST") return json$2(res, 405, { error: "method not allowed" });
1090
+ if (req.method !== "POST") return json$4(res, 405, { error: "method not allowed" });
1068
1091
  if (!await authorize(req, res)) return;
1069
1092
  try {
1070
1093
  await auth.signOut();
1071
- json$2(res, 200, { ok: true });
1094
+ json$4(res, 200, { ok: true });
1072
1095
  } catch (error) {
1073
- json$2(res, 500, { error: safeMessage(error) });
1096
+ json$4(res, 500, { error: safeMessage(error) });
1074
1097
  }
1075
1098
  }
1076
1099
  }),
@@ -1078,22 +1101,22 @@ function registerOpenAICodexAuthRoutes(ctx, store, trustedOriginsOverride, fastM
1078
1101
  kind: "exact",
1079
1102
  path: OPENAI_CODEX_FAST_MODE_PATH,
1080
1103
  handler: async (req, res) => {
1081
- if (req.method !== "GET" && req.method !== "POST") return json$2(res, 405, { error: "method not allowed" });
1104
+ if (req.method !== "GET" && req.method !== "POST") return json$4(res, 405, { error: "method not allowed" });
1082
1105
  if (!await authorize(req, res)) return;
1083
1106
  if (req.method === "GET") {
1084
1107
  const sessionId = fastModeSessionIdFromQuery(req);
1085
- if (sessionId === void 0) return json$2(res, 400, { error: "invalid input" });
1086
- return json$2(res, 200, { enabled: fastMode.isEnabled(sessionId) });
1108
+ if (sessionId === void 0) return json$4(res, 400, { error: "invalid input" });
1109
+ return json$4(res, 200, { enabled: fastMode.isEnabled(sessionId) });
1087
1110
  }
1088
1111
  const type = header(req, "content-type");
1089
- if (type === void 0 || !/^application\/json(?:\s*;|$)/iu.test(type.trim())) return json$2(res, 415, { error: "unsupported content type" });
1112
+ if (type === void 0 || !/^application\/json(?:\s*;|$)/iu.test(type.trim())) return json$4(res, 415, { error: "unsupported content type" });
1090
1113
  try {
1091
1114
  const body = fastModeBody(await readFastModeBody(req));
1092
- if (body === void 0) return json$2(res, 400, { error: "invalid input" });
1115
+ if (body === void 0) return json$4(res, 400, { error: "invalid input" });
1093
1116
  fastMode.set(body.sessionId, body.enabled);
1094
- return json$2(res, 200, { enabled: fastMode.isEnabled(body.sessionId) });
1117
+ return json$4(res, 200, { enabled: fastMode.isEnabled(body.sessionId) });
1095
1118
  } catch (error) {
1096
- return json$2(res, error instanceof RangeError ? 413 : 400, { error: error instanceof RangeError ? "request body too large" : "invalid input" });
1119
+ return json$4(res, error instanceof RangeError ? 413 : 400, { error: error instanceof RangeError ? "request body too large" : "invalid input" });
1097
1120
  }
1098
1121
  }
1099
1122
  })
@@ -1105,6 +1128,389 @@ function registerOpenAICodexAuthRoutes(ctx, store, trustedOriginsOverride, fastM
1105
1128
  }, "dsh-codex-connect: Web OAuth routes");
1106
1129
  }
1107
1130
  //#endregion
1131
+ //#region src/settings-contract.ts
1132
+ /** Node-free settings contract shared by the Host plugin and browser card. */
1133
+ /** Stable Harness settings namespace owned by this plugin. */
1134
+ const OPENAI_CODEX_SETTINGS_NAMESPACE = "llm-openai-codex";
1135
+ /** Suggested local HTTP proxy shown by the settings UI; it is never enabled by default. */
1136
+ const DEFAULT_OPENAI_CODEX_PROXY_URL = "http://127.0.0.1:7890";
1137
+ /**
1138
+ * Normalize the credential-free HTTP proxy URL accepted by Codex Connect.
1139
+ * Paths, query strings, fragments, and embedded credentials are rejected so
1140
+ * the value remains an origin rather than an opaque request target.
1141
+ */
1142
+ function normalizeOpenAICodexProxyUrl(value) {
1143
+ if (typeof value !== "string" || value.trim().length === 0) return void 0;
1144
+ try {
1145
+ const parsed = new URL(value.trim());
1146
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
1147
+ if (parsed.username !== "" || parsed.password !== "") return void 0;
1148
+ if (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") return void 0;
1149
+ if (parsed.hostname.length === 0) return void 0;
1150
+ if (parsed.port !== "" && (!/^\d+$/u.test(parsed.port) || Number(parsed.port) < 1 || Number(parsed.port) > 65535)) return void 0;
1151
+ return parsed.origin;
1152
+ } catch {
1153
+ return;
1154
+ }
1155
+ }
1156
+ /** Whether a value is a supported, canonical HTTP(S) proxy origin. */
1157
+ function isValidOpenAICodexProxyUrl(value) {
1158
+ return normalizeOpenAICodexProxyUrl(value) !== void 0;
1159
+ }
1160
+ /** Default model used by the standalone search endpoint. */
1161
+ const DEFAULT_OPENAI_CODEX_SEARCH_MODEL = "gpt-5.6-sol";
1162
+ /** Default search mode, matching the official local Codex client. */
1163
+ const DEFAULT_OPENAI_CODEX_SEARCH_MODE = "cached";
1164
+ /** Default provider search-context size. */
1165
+ const DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE = "medium";
1166
+ /** Default output budget for the standalone search response. */
1167
+ const DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS = 1e4;
1168
+ const DEFAULT_OPENAI_CODEX_SETTINGS = Object.freeze({
1169
+ models: void 0,
1170
+ enableProxy: false,
1171
+ proxyUrl: DEFAULT_OPENAI_CODEX_PROXY_URL,
1172
+ enableSearch: false,
1173
+ enableImageTool: false,
1174
+ enableImageGeneration: false,
1175
+ searchModel: DEFAULT_OPENAI_CODEX_SEARCH_MODEL,
1176
+ searchMode: DEFAULT_OPENAI_CODEX_SEARCH_MODE,
1177
+ searchContextSize: DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE,
1178
+ searchMaxOutputTokens: DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS
1179
+ });
1180
+ /** Fill the schema defaults even when called without Cordis validation. */
1181
+ function resolveOpenAICodexSettings(value) {
1182
+ const resolved = {
1183
+ ...DEFAULT_OPENAI_CODEX_SETTINGS,
1184
+ ...value
1185
+ };
1186
+ if (!isValidOpenAICodexProxyUrl(resolved.proxyUrl)) throw new TypeError("OpenAI Codex proxyUrl must be an HTTP(S) origin without credentials or a path");
1187
+ return resolved;
1188
+ }
1189
+ /** Resolve the active proxy without treating a disabled value as a route. */
1190
+ function resolveOpenAICodexProxyUrl(value) {
1191
+ const resolved = resolveOpenAICodexSettings(value);
1192
+ return resolved.enableProxy ? normalizeOpenAICodexProxyUrl(resolved.proxyUrl) : void 0;
1193
+ }
1194
+ function isRecord$1(value) {
1195
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1196
+ }
1197
+ /** Narrow the redacted settings wire payload before it enters React state. */
1198
+ function decodeOpenAICodexSettings(value) {
1199
+ if (!isRecord$1(value)) return void 0;
1200
+ const models = value["models"];
1201
+ const enableProxy = value["enableProxy"];
1202
+ const proxyUrl = value["proxyUrl"];
1203
+ const enableSearch = value["enableSearch"];
1204
+ const enableImageTool = value["enableImageTool"];
1205
+ const enableImageGeneration = value["enableImageGeneration"];
1206
+ const searchModel = value["searchModel"];
1207
+ const searchMode = value["searchMode"];
1208
+ const searchContextSize = value["searchContextSize"];
1209
+ const searchMaxOutputTokens = value["searchMaxOutputTokens"];
1210
+ if (models !== void 0 && (!Array.isArray(models) || models.some((model) => typeof model !== "string"))) return void 0;
1211
+ if (enableProxy !== void 0 && typeof enableProxy !== "boolean") return void 0;
1212
+ if (proxyUrl !== void 0 && (typeof proxyUrl !== "string" || !isValidOpenAICodexProxyUrl(proxyUrl))) return void 0;
1213
+ if (typeof enableSearch !== "boolean" || typeof enableImageTool !== "boolean") return void 0;
1214
+ if (enableImageGeneration !== void 0 && typeof enableImageGeneration !== "boolean") return void 0;
1215
+ if (typeof searchModel !== "string" || searchModel.trim().length === 0) return void 0;
1216
+ if (searchMode !== "cached" && searchMode !== "indexed" && searchMode !== "live") return void 0;
1217
+ if (searchContextSize !== "low" && searchContextSize !== "medium" && searchContextSize !== "high") return void 0;
1218
+ if (typeof searchMaxOutputTokens !== "number" || !Number.isInteger(searchMaxOutputTokens) || searchMaxOutputTokens < 1) return void 0;
1219
+ return {
1220
+ models: models === void 0 ? void 0 : [...new Set(models)],
1221
+ enableProxy: enableProxy ?? false,
1222
+ proxyUrl: proxyUrl === void 0 ? DEFAULT_OPENAI_CODEX_PROXY_URL : normalizeOpenAICodexProxyUrl(proxyUrl),
1223
+ enableSearch,
1224
+ enableImageTool,
1225
+ enableImageGeneration: enableImageGeneration ?? false,
1226
+ searchModel,
1227
+ searchMode,
1228
+ searchContextSize,
1229
+ searchMaxOutputTokens
1230
+ };
1231
+ }
1232
+ //#endregion
1233
+ //#region src/provider-proxy.ts
1234
+ /** Explicit Codex-only HTTP(S) proxying, probing, and lifecycle ownership. */
1235
+ /** Canonical first-party endpoint used for a no-auth, no-model reachability probe. */
1236
+ const OPENAI_CODEX_PROXY_PROBE_URL = "https://chatgpt.com/backend-api/codex";
1237
+ /** Upper bound for one candidate probe, including CONNECT and response headers. */
1238
+ const OPENAI_CODEX_PROXY_PROBE_TIMEOUT_MS = 3e3;
1239
+ /** Maximum number of candidates considered by automatic detection. */
1240
+ const OPENAI_CODEX_PROXY_CANDIDATE_LIMIT = 8;
1241
+ /** Bounded local candidates documented by the settings UI. */
1242
+ const OPENAI_CODEX_LOCAL_PROXY_CANDIDATES = [
1243
+ "http://127.0.0.1:7890",
1244
+ "http://127.0.0.1:7897",
1245
+ "http://127.0.0.1:10809"
1246
+ ];
1247
+ const proxyScope = new AsyncLocalStorage();
1248
+ const activeOwners = /* @__PURE__ */ new Set();
1249
+ var ScopedProxyDispatcher = class extends Dispatcher {
1250
+ fallback;
1251
+ constructor(fallback) {
1252
+ super();
1253
+ this.fallback = fallback;
1254
+ }
1255
+ setFallback(fallback) {
1256
+ this.fallback = fallback;
1257
+ }
1258
+ dispatch(options, handler) {
1259
+ return (proxyScope.getStore() ?? this.fallback).dispatch(options, handler);
1260
+ }
1261
+ };
1262
+ let installedDispatcher;
1263
+ let previousDispatcher;
1264
+ function ensureInstalled(owner) {
1265
+ if (activeOwners.has(owner)) return;
1266
+ const current = getGlobalDispatcher();
1267
+ if (installedDispatcher === void 0) {
1268
+ previousDispatcher = current;
1269
+ installedDispatcher = new ScopedProxyDispatcher(current);
1270
+ setGlobalDispatcher(installedDispatcher);
1271
+ } else if (current !== installedDispatcher) {
1272
+ installedDispatcher.setFallback(current);
1273
+ setGlobalDispatcher(installedDispatcher);
1274
+ }
1275
+ activeOwners.add(owner);
1276
+ }
1277
+ function removeOwner(owner) {
1278
+ activeOwners.delete(owner);
1279
+ if (activeOwners.size !== 0 || installedDispatcher === void 0) return;
1280
+ const installed = installedDispatcher;
1281
+ const previous = previousDispatcher;
1282
+ installedDispatcher = void 0;
1283
+ previousDispatcher = void 0;
1284
+ if (getGlobalDispatcher() === installed && previous !== void 0) setGlobalDispatcher(previous);
1285
+ }
1286
+ function isPromiseLike(value) {
1287
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
1288
+ }
1289
+ function errorCode(error) {
1290
+ if (typeof error !== "object" || error === null) return void 0;
1291
+ const record = error;
1292
+ return typeof record["code"] === "string" ? record["code"] : errorCode(record["cause"]);
1293
+ }
1294
+ function classifyProbeError(error) {
1295
+ const code = errorCode(error);
1296
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "dns-failure";
1297
+ if (code === "ECONNREFUSED") return "connection-refused";
1298
+ if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ABORT_ERR") return "timeout";
1299
+ if (code === "ERR_TLS_CERT_ALTNAME_INVALID" || code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" || code === "DEPTH_ZERO_SELF_SIGNED_CERT" || code === "ERR_TLS_CERT_SIGNATURE_ALGORITHM_UNSUPPORTED") return "tls-failure";
1300
+ return "connect-failure";
1301
+ }
1302
+ function classifyResponse(status) {
1303
+ if (status === 407) return "proxy-authentication-required";
1304
+ if (status === 401 || status === 403) return "upstream-authentication-required";
1305
+ return "reachable";
1306
+ }
1307
+ function candidateEnvironmentValues() {
1308
+ return [
1309
+ process.env["HTTPS_PROXY"],
1310
+ process.env["https_proxy"],
1311
+ process.env["HTTP_PROXY"],
1312
+ process.env["http_proxy"],
1313
+ process.env["ALL_PROXY"],
1314
+ process.env["all_proxy"]
1315
+ ].filter((value) => value !== void 0);
1316
+ }
1317
+ /** Return a small, deterministic candidate set; this never scans LAN hosts or ports. */
1318
+ function listOpenAICodexProxyCandidates() {
1319
+ const candidates = [];
1320
+ for (const value of [...candidateEnvironmentValues(), ...OPENAI_CODEX_LOCAL_PROXY_CANDIDATES]) {
1321
+ const normalized = normalizeOpenAICodexProxyUrl(value);
1322
+ if (normalized !== void 0 && !candidates.includes(normalized)) candidates.push(normalized);
1323
+ if (candidates.length >= 8) break;
1324
+ }
1325
+ return candidates;
1326
+ }
1327
+ /** One plugin instance owns its proxy agents and contributes one global wrapper owner. */
1328
+ var OpenAICodexProxyManager = class {
1329
+ agents = /* @__PURE__ */ new Map();
1330
+ activeOperations = 0;
1331
+ idleWaiters = [];
1332
+ disposed = false;
1333
+ disposePromise;
1334
+ async waitForIdle() {
1335
+ if (this.activeOperations === 0) return;
1336
+ await new Promise((resolve) => {
1337
+ this.idleWaiters.push(resolve);
1338
+ });
1339
+ }
1340
+ async closeAgents() {
1341
+ removeOwner(this);
1342
+ const agents = [...this.agents.values()];
1343
+ this.agents.clear();
1344
+ await Promise.allSettled(agents.map((agent) => agent.close()));
1345
+ }
1346
+ agentFor(proxyUrl) {
1347
+ let agent = this.agents.get(proxyUrl);
1348
+ if (agent !== void 0) return agent;
1349
+ agent = new ProxyAgent(proxyUrl);
1350
+ this.agents.set(proxyUrl, agent);
1351
+ return agent;
1352
+ }
1353
+ acquire(proxyUrl) {
1354
+ if (this.disposed) throw new Error("OpenAI Codex proxy manager has been disposed");
1355
+ ensureInstalled(this);
1356
+ this.activeOperations += 1;
1357
+ let released = false;
1358
+ return {
1359
+ agent: this.agentFor(proxyUrl),
1360
+ release: () => {
1361
+ if (released) return;
1362
+ released = true;
1363
+ this.activeOperations -= 1;
1364
+ if (this.activeOperations === 0) for (const resolve of this.idleWaiters.splice(0)) resolve();
1365
+ }
1366
+ };
1367
+ }
1368
+ /** Run a synchronous or asynchronous Codex operation in the selected proxy scope. */
1369
+ run(proxyUrl, operation) {
1370
+ if (proxyUrl === void 0) return operation();
1371
+ const normalized = normalizeOpenAICodexProxyUrl(proxyUrl);
1372
+ if (!isValidOpenAICodexProxyUrl(normalized)) throw new TypeError("OpenAI Codex proxy URL is invalid");
1373
+ const lease = this.acquire(normalized);
1374
+ try {
1375
+ const value = proxyScope.run(lease.agent, operation);
1376
+ if (isPromiseLike(value)) return Promise.resolve(value).finally(lease.release);
1377
+ lease.release();
1378
+ return value;
1379
+ } catch (error) {
1380
+ lease.release();
1381
+ throw error;
1382
+ }
1383
+ }
1384
+ /** Run a streaming operation and keep the proxy lease until its final event. */
1385
+ runStream(proxyUrl, operation) {
1386
+ if (proxyUrl === void 0) return operation();
1387
+ const normalized = normalizeOpenAICodexProxyUrl(proxyUrl);
1388
+ if (!isValidOpenAICodexProxyUrl(normalized)) throw new TypeError("OpenAI Codex proxy URL is invalid");
1389
+ const lease = this.acquire(normalized);
1390
+ try {
1391
+ const stream = proxyScope.run(lease.agent, operation);
1392
+ Promise.resolve(stream.result()).finally(lease.release);
1393
+ return stream;
1394
+ } catch (error) {
1395
+ lease.release();
1396
+ throw error;
1397
+ }
1398
+ }
1399
+ /** Probe one proxy without credentials, model calls, quota calls, or settings writes. */
1400
+ async probe(proxyUrl) {
1401
+ const normalized = normalizeOpenAICodexProxyUrl(proxyUrl);
1402
+ if (normalized === void 0) return {
1403
+ proxyUrl,
1404
+ reachable: false,
1405
+ classification: "invalid"
1406
+ };
1407
+ try {
1408
+ const response = await this.run(normalized, () => fetch(OPENAI_CODEX_PROXY_PROBE_URL, {
1409
+ method: "GET",
1410
+ redirect: "manual",
1411
+ headers: { accept: "application/json" },
1412
+ signal: AbortSignal.timeout(OPENAI_CODEX_PROXY_PROBE_TIMEOUT_MS)
1413
+ }));
1414
+ await response.body?.cancel();
1415
+ return {
1416
+ proxyUrl: normalized,
1417
+ reachable: true,
1418
+ classification: classifyResponse(response.status),
1419
+ status: response.status
1420
+ };
1421
+ } catch (error) {
1422
+ return {
1423
+ proxyUrl: normalized,
1424
+ reachable: false,
1425
+ classification: classifyProbeError(error)
1426
+ };
1427
+ }
1428
+ }
1429
+ /** Close owned pools only after all scoped operations have become quiescent. */
1430
+ async dispose() {
1431
+ if (this.disposePromise !== void 0) return this.disposePromise;
1432
+ this.disposed = true;
1433
+ this.disposePromise = (async () => {
1434
+ await this.waitForIdle();
1435
+ await this.closeAgents();
1436
+ })();
1437
+ return this.disposePromise;
1438
+ }
1439
+ /** Release the process wrapper and pools after the user disables the proxy. */
1440
+ async deactivate() {
1441
+ if (this.disposed) return;
1442
+ await this.waitForIdle();
1443
+ await this.closeAgents();
1444
+ }
1445
+ };
1446
+ /** Probe the bounded automatic candidate set in parallel. */
1447
+ async function detectOpenAICodexProxies(manager) {
1448
+ return Promise.all(listOpenAICodexProxyCandidates().map((candidate) => manager.probe(candidate)));
1449
+ }
1450
+ //#endregion
1451
+ //#region src/proxy-paths.ts
1452
+ /** Node-free route constants shared by the Host and browser plugin halves. */
1453
+ /** Detect bounded local/environment proxy candidates without changing settings. */
1454
+ const OPENAI_CODEX_PROXY_DETECT_PATH = "/plugins/dsh-openai-codex/proxy/detect";
1455
+ /** Test one manually entered proxy origin without changing settings. */
1456
+ const OPENAI_CODEX_PROXY_TEST_PATH = "/plugins/dsh-openai-codex/proxy/test";
1457
+ //#endregion
1458
+ //#region src/proxy-routes.ts
1459
+ function json$3(res, status, value) {
1460
+ res.writeHead(status, {
1461
+ "content-type": "application/json; charset=utf-8",
1462
+ "cache-control": "no-store",
1463
+ "x-content-type-options": "nosniff"
1464
+ });
1465
+ res.end(JSON.stringify(value));
1466
+ }
1467
+ function proxyUrlFromQuery(req) {
1468
+ if (typeof req.url !== "string") return void 0;
1469
+ try {
1470
+ const values = new URL(req.url, "http://dsh.invalid").searchParams.getAll("proxyUrl");
1471
+ return values.length === 1 && values[0].length <= 2e3 ? values[0] : void 0;
1472
+ } catch {
1473
+ return;
1474
+ }
1475
+ }
1476
+ /** Register bounded detection and draft-probe routes; neither route mutates settings. */
1477
+ function registerOpenAICodexProxyRoutes(ctx, trustedOrigins, manager) {
1478
+ ctx.effect(() => {
1479
+ const authorize = async (req, res) => {
1480
+ const decision = await trustedRequestDecision(req, trustedOrigins);
1481
+ if (decision.trusted) return true;
1482
+ json$3(res, 403, { error: decision.error });
1483
+ return false;
1484
+ };
1485
+ const routes = [ctx.webServer.register({
1486
+ kind: "exact",
1487
+ path: OPENAI_CODEX_PROXY_DETECT_PATH,
1488
+ handler: async (req, res) => {
1489
+ if (req.method !== "POST") return json$3(res, 405, { error: "method not allowed" });
1490
+ if (!await authorize(req, res)) return;
1491
+ const results = await detectOpenAICodexProxies(manager);
1492
+ return json$3(res, 200, {
1493
+ candidates: results.filter((result) => result.reachable),
1494
+ results
1495
+ });
1496
+ }
1497
+ }), ctx.webServer.register({
1498
+ kind: "exact",
1499
+ path: OPENAI_CODEX_PROXY_TEST_PATH,
1500
+ handler: async (req, res) => {
1501
+ if (req.method !== "POST") return json$3(res, 405, { error: "method not allowed" });
1502
+ if (!await authorize(req, res)) return;
1503
+ const proxyUrl = proxyUrlFromQuery(req);
1504
+ if (proxyUrl === void 0) return json$3(res, 400, { error: "invalid proxy URL" });
1505
+ return json$3(res, 200, await manager.probe(proxyUrl));
1506
+ }
1507
+ })];
1508
+ return () => {
1509
+ for (const dispose of routes) dispose();
1510
+ };
1511
+ }, "dsh-codex-connect: proxy detection routes");
1512
+ }
1513
+ //#endregion
1108
1514
  //#region src/compatibility.ts
1109
1515
  const COMPATIBILITY_SCHEMA_VERSION = 1;
1110
1516
  const SUPPORTED_NODE_RANGE = "^22.19.0 || >=24.0.0";
@@ -1211,7 +1617,12 @@ function evaluateCompatibility(input = {}) {
1211
1617
  }
1212
1618
  /** Alias for callers that prefer assessment terminology. */
1213
1619
  const assessCompatibility = evaluateCompatibility;
1214
- async function readPackageVersionFromEntry(name) {
1620
+ /**
1621
+ * Resolve installed package metadata without returning a filesystem path.
1622
+ * @param name - package to resolve from this plugin installation.
1623
+ * @returns its version, or undefined when metadata cannot be read.
1624
+ */
1625
+ async function readInstalledPackageVersion(name) {
1215
1626
  let entry;
1216
1627
  try {
1217
1628
  const resolved = import.meta.resolve(name);
@@ -1234,7 +1645,7 @@ async function readPackageVersionFromEntry(name) {
1234
1645
  }
1235
1646
  /** Read installed package metadata and return only versions and statuses. */
1236
1647
  async function detectCompatibility(options = {}) {
1237
- const readVersion = options.readPackageVersion ?? readPackageVersionFromEntry;
1648
+ const readVersion = options.readPackageVersion ?? readInstalledPackageVersion;
1238
1649
  const packageVersions = options.packageVersions ?? options.packages ?? options.installed?.packages;
1239
1650
  const resolvedPackages = packageVersions === void 0 ? Object.fromEntries(await Promise.all(COMPATIBILITY_PACKAGES.map(async (name) => [name, await readVersion(name)]))) : packageVersions;
1240
1651
  return evaluateCompatibility({
@@ -1259,7 +1670,8 @@ const HIGHLIGHT_KINDS = [
1259
1670
  "search-stability",
1260
1671
  "image-generation",
1261
1672
  "oauth-history",
1262
- "model-visibility"
1673
+ "model-visibility",
1674
+ "proxy-connection"
1263
1675
  ];
1264
1676
  function isHighlightKind(value) {
1265
1677
  return typeof value === "string" && HIGHLIGHT_KINDS.includes(value);
@@ -1688,7 +2100,7 @@ async function detectCurrentDshVersion() {
1688
2100
  const adapter = report.packages["@deepseek-ai/dsh-llm-pi-ai"].installed;
1689
2101
  return llm !== null && llm === adapter ? llm : void 0;
1690
2102
  }
1691
- function json$1(res, status, value) {
2103
+ function json$2(res, status, value) {
1692
2104
  res.writeHead(status, {
1693
2105
  "content-type": "application/json; charset=utf-8",
1694
2106
  "cache-control": "no-store",
@@ -1703,14 +2115,14 @@ function registerOpenAICodexUpdateRoutes(ctx, options, trustedOrigins) {
1703
2115
  kind: "exact",
1704
2116
  path: OPENAI_CODEX_RUNTIME_PATH,
1705
2117
  handler: async (req, res) => {
1706
- if (req.method !== "GET") return json$1(res, 405, { error: "method not allowed" });
2118
+ if (req.method !== "GET") return json$2(res, 405, { error: "method not allowed" });
1707
2119
  const decision = await trustedRequestDecision(req, trustedOrigins);
1708
- if (!decision.trusted) return json$1(res, 403, { error: decision.error });
2120
+ if (!decision.trusted) return json$2(res, 403, { error: decision.error });
1709
2121
  try {
1710
2122
  const currentDshVersion = await (options.resolveCurrentDshVersion ?? detectCurrentDshVersion)();
1711
- return json$1(res, 200, currentDshVersion === void 0 ? {} : { currentDshVersion });
2123
+ return json$2(res, 200, currentDshVersion === void 0 ? {} : { currentDshVersion });
1712
2124
  } catch {
1713
- return json$1(res, 200, {});
2125
+ return json$2(res, 200, {});
1714
2126
  }
1715
2127
  }
1716
2128
  });
@@ -1718,9 +2130,9 @@ function registerOpenAICodexUpdateRoutes(ctx, options, trustedOrigins) {
1718
2130
  kind: "exact",
1719
2131
  path: OPENAI_CODEX_UPDATE_PATH,
1720
2132
  handler: async (req, res) => {
1721
- if (req.method !== "GET") return json$1(res, 405, { error: "method not allowed" });
2133
+ if (req.method !== "GET") return json$2(res, 405, { error: "method not allowed" });
1722
2134
  const decision = await trustedRequestDecision(req, trustedOrigins);
1723
- if (!decision.trusted) return json$1(res, 403, { error: decision.error });
2135
+ if (!decision.trusted) return json$2(res, 403, { error: decision.error });
1724
2136
  let result;
1725
2137
  let currentDshVersion;
1726
2138
  try {
@@ -1743,7 +2155,7 @@ function registerOpenAICodexUpdateRoutes(ctx, options, trustedOrigins) {
1743
2155
  reason: "registry-unavailable"
1744
2156
  };
1745
2157
  }
1746
- return json$1(res, 200, result);
2158
+ return json$2(res, 200, result);
1747
2159
  }
1748
2160
  });
1749
2161
  return () => {
@@ -1759,7 +2171,7 @@ function registerOpenAICodexUpdateRoutes(ctx, options, trustedOrigins) {
1759
2171
  const OPENAI_CODEX_MODEL_CATALOG_PATH = "/plugins/dsh-codex-connect/models";
1760
2172
  //#endregion
1761
2173
  //#region src/model-routes.ts
1762
- function json(res, status, value) {
2174
+ function json$1(res, status, value) {
1763
2175
  res.writeHead(status, {
1764
2176
  "content-type": "application/json; charset=utf-8",
1765
2177
  "cache-control": "no-store",
@@ -1772,17 +2184,162 @@ function registerOpenAICodexModelCatalogRoute(ctx, resolveCatalog, trustedOrigin
1772
2184
  ctx.effect(() => ctx.webServer.register({
1773
2185
  kind: "exact",
1774
2186
  path: OPENAI_CODEX_MODEL_CATALOG_PATH,
2187
+ handler: async (req, res) => {
2188
+ if (req.method !== "GET") return json$1(res, 405, { error: "method not allowed" });
2189
+ const decision = await trustedRequestDecision(req, trustedOrigins);
2190
+ if (!decision.trusted) return json$1(res, 403, { error: decision.error });
2191
+ return json$1(res, 200, resolveCatalog());
2192
+ }
2193
+ }), "dsh-codex-connect: model catalog route");
2194
+ }
2195
+ //#endregion
2196
+ //#region src/image-assets-contract.ts
2197
+ /** Same-origin endpoint serving one session-owned original generated image. */
2198
+ const OPENAI_CODEX_ORIGINAL_IMAGE_PATH = "/plugins/dsh-codex-connect/images/original";
2199
+ /** Opaque identifier format for one plugin-owned original image. */
2200
+ const OPENAI_CODEX_IMAGE_ASSET_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
2201
+ function positiveSafeInteger$3(value) {
2202
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
2203
+ }
2204
+ /** Decode session-log metadata without trusting an asset id, filename, or media type. */
2205
+ function decodeOpenAICodexOriginalImageRef(value) {
2206
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
2207
+ const candidate = value;
2208
+ if (typeof candidate.assetId !== "string" || !OPENAI_CODEX_IMAGE_ASSET_ID_PATTERN.test(candidate.assetId) || candidate.mediaType !== "image/png" && candidate.mediaType !== "image/jpeg" && candidate.mediaType !== "image/webp" || !positiveSafeInteger$3(candidate.width) || !positiveSafeInteger$3(candidate.height) || !positiveSafeInteger$3(candidate.bytes) || candidate.bytes > 50331648 || typeof candidate.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(candidate.name) || typeof candidate.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(candidate.sha256)) return void 0;
2209
+ return {
2210
+ assetId: candidate.assetId,
2211
+ mediaType: candidate.mediaType,
2212
+ width: candidate.width,
2213
+ height: candidate.height,
2214
+ bytes: candidate.bytes,
2215
+ name: candidate.name,
2216
+ sha256: candidate.sha256
2217
+ };
2218
+ }
2219
+ //#endregion
2220
+ //#region src/image-presentation.ts
2221
+ /** Stable metadata marker for generated image result views. */
2222
+ const IMAGE_PRESENTATION_KIND = "codex-connect-images";
2223
+ function positiveSafeInteger$2(value) {
2224
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
2225
+ }
2226
+ function mediaType$1(value) {
2227
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp";
2228
+ }
2229
+ function imageRef(value) {
2230
+ if (typeof value !== "object" || value === null) return void 0;
2231
+ const candidate = value;
2232
+ if (typeof candidate.attachmentId !== "string" || candidate.attachmentId.length === 0 || !mediaType$1(candidate.mediaType) || !positiveSafeInteger$2(candidate.bytes) || !positiveSafeInteger$2(candidate.width) || !positiveSafeInteger$2(candidate.height) || candidate.name !== void 0 && (typeof candidate.name !== "string" || candidate.name.length === 0)) return void 0;
2233
+ return {
2234
+ attachmentId: candidate.attachmentId,
2235
+ mediaType: candidate.mediaType,
2236
+ bytes: candidate.bytes,
2237
+ width: candidate.width,
2238
+ height: candidate.height,
2239
+ ...candidate.name === void 0 ? {} : { name: candidate.name }
2240
+ };
2241
+ }
2242
+ /** Decode durable tool-result metadata without trusting arbitrary session JSON. */
2243
+ function decodeImagePresentationMeta(value) {
2244
+ if (typeof value !== "object" || value === null) return void 0;
2245
+ const candidate = value;
2246
+ if (candidate.kind !== "codex-connect-images" || typeof candidate.prompt !== "string" || candidate.prompt.length < 1 || candidate.prompt.length > 32e3 || !Array.isArray(candidate.images) || candidate.images.length < 1 || candidate.images.length > 4) return void 0;
2247
+ if (candidate.schemaVersion === void 0) {
2248
+ const previews = candidate.images.map(imageRef);
2249
+ if (previews.some((image) => image === void 0)) return void 0;
2250
+ return {
2251
+ kind: IMAGE_PRESENTATION_KIND,
2252
+ schemaVersion: 1,
2253
+ prompt: candidate.prompt,
2254
+ images: previews.map((preview) => ({ preview }))
2255
+ };
2256
+ }
2257
+ if (candidate.schemaVersion !== 1) return void 0;
2258
+ const images = [];
2259
+ for (const value of candidate.images) {
2260
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
2261
+ const entry = value;
2262
+ const preview = imageRef(entry.preview);
2263
+ const original = decodeOpenAICodexOriginalImageRef(entry.original);
2264
+ if (preview === void 0 || original === void 0) return void 0;
2265
+ images.push({
2266
+ preview,
2267
+ original
2268
+ });
2269
+ }
2270
+ return {
2271
+ kind: IMAGE_PRESENTATION_KIND,
2272
+ schemaVersion: 1,
2273
+ prompt: candidate.prompt,
2274
+ images
2275
+ };
2276
+ }
2277
+ //#endregion
2278
+ //#region src/image-asset-routes.ts
2279
+ /** Fork access follows copied result events, not every asset owned by an ancestor. */
2280
+ function inheritedOriginal(session, assetId) {
2281
+ if (session?.header.parentSession === void 0) return void 0;
2282
+ const seedLength = session.header.seedLength ?? 0;
2283
+ for (const event of session.events) {
2284
+ if (event.seq >= seedLength) break;
2285
+ if (event.type !== "tool/result") continue;
2286
+ const original = decodeImagePresentationMeta(event.data.meta)?.images.find((image) => image.original?.assetId === assetId)?.original;
2287
+ if (original !== void 0) return original;
2288
+ }
2289
+ }
2290
+ function json(res, status, value) {
2291
+ res.writeHead(status, {
2292
+ "content-type": "application/json; charset=utf-8",
2293
+ "cache-control": "no-store",
2294
+ "x-content-type-options": "nosniff"
2295
+ });
2296
+ res.end(JSON.stringify(value));
2297
+ }
2298
+ function query(req) {
2299
+ if (typeof req.url !== "string") return void 0;
2300
+ try {
2301
+ const parsed = new URL(req.url, "http://dsh.invalid");
2302
+ const sessionIds = parsed.searchParams.getAll("sessionId");
2303
+ const assetIds = parsed.searchParams.getAll("assetId");
2304
+ const sessionId = sessionIds[0];
2305
+ const assetId = assetIds[0];
2306
+ if (sessionIds.length !== 1 || assetIds.length !== 1 || sessionId === void 0 || sessionId.length < 1 || sessionId.length > 512 || assetId === void 0 || !OPENAI_CODEX_IMAGE_ASSET_ID_PATTERN.test(assetId)) return void 0;
2307
+ return {
2308
+ sessionId,
2309
+ assetId
2310
+ };
2311
+ } catch {
2312
+ return;
2313
+ }
2314
+ }
2315
+ /** Register a history-safe route even while new image generation is disabled. */
2316
+ function registerOpenAICodexOriginalImageRoute(ctx, trustedOrigins, assets) {
2317
+ ctx.effect(() => ctx.webServer.register({
2318
+ kind: "exact",
2319
+ path: OPENAI_CODEX_ORIGINAL_IMAGE_PATH,
1775
2320
  handler: async (req, res) => {
1776
2321
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
1777
2322
  const decision = await trustedRequestDecision(req, trustedOrigins);
1778
2323
  if (!decision.trusted) return json(res, 403, { error: decision.error });
1779
- return json(res, 200, resolveCatalog());
2324
+ const requested = query(req);
2325
+ if (requested === void 0) return json(res, 400, { error: "invalid input" });
2326
+ const session = ctx.get("sessions")?.get(SessionId(requested.sessionId));
2327
+ const stored = await assets.read(requested.sessionId, requested.assetId, inheritedOriginal(session, requested.assetId));
2328
+ if (stored === void 0) return json(res, 404, { error: "original image not found" });
2329
+ res.writeHead(200, {
2330
+ "content-type": stored.ref.mediaType,
2331
+ "content-length": String(stored.ref.bytes),
2332
+ "content-disposition": `attachment; filename="${stored.ref.name}"`,
2333
+ "cache-control": "private, no-store",
2334
+ "x-content-type-options": "nosniff"
2335
+ });
2336
+ res.end(Buffer.from(stored.data));
1780
2337
  }
1781
- }), "dsh-codex-connect: model catalog route");
2338
+ }), "dsh-codex-connect: original image download route");
1782
2339
  }
1783
2340
  //#endregion
1784
2341
  //#region src/version.ts
1785
- const CODEX_CONNECT_VERSION = "0.1.0-alpha.4.19";
2342
+ const CODEX_CONNECT_VERSION = "0.1.0-alpha.4.21";
1786
2343
  //#endregion
1787
2344
  //#region src/doctor.ts
1788
2345
  /** Secret-free diagnostics and duplicate-provider guidance. */
@@ -1964,10 +2521,6 @@ function detectEncodedImage(data) {
1964
2521
  }
1965
2522
  }
1966
2523
  //#endregion
1967
- //#region src/image-presentation.ts
1968
- /** Stable metadata marker for generated image result views. */
1969
- const IMAGE_PRESENTATION_KIND = "codex-connect-images";
1970
- //#endregion
1971
2524
  //#region src/image-tool.ts
1972
2525
  /** Stable model-callable tool name. */
1973
2526
  const IMAGE_GENERATE_TOOL_NAME = "codex_connect_image_generate";
@@ -1999,29 +2552,41 @@ function extension(mediaType) {
1999
2552
  return mediaType === "image/jpeg" ? "jpg" : mediaType.slice(6);
2000
2553
  }
2001
2554
  function outputContent(value) {
2002
- const lines = value.images.map((image, index) => `${String(index + 1)}. ${image.mediaType}, ${String(image.width)}x${String(image.height)} px, ${String(image.bytes)} bytes, attachment ${image.attachmentId}`);
2555
+ const lines = value.images.map(({ original, preview }, index) => `${String(index + 1)}. original ${original.mediaType}, ${String(original.width)}x${String(original.height)} px, ${String(original.bytes)} bytes; preview ${String(preview.width)}x${String(preview.height)} px, attachment ${preview.attachmentId}`);
2003
2556
  return [{
2004
2557
  type: "text",
2005
2558
  text: `Generated ${String(value.images.length)} image${value.images.length === 1 ? "" : "s"}:\n${lines.join("\n")}`
2006
- }, ...value.images.map((image) => ({
2559
+ }, ...value.images.map(({ preview }) => ({
2007
2560
  type: "image",
2008
2561
  attachment: {
2009
- attachmentId: AttachmentId(image.attachmentId),
2010
- mediaType: image.mediaType,
2011
- width: image.width,
2012
- height: image.height,
2013
- bytes: image.bytes,
2014
- name: image.name
2562
+ attachmentId: AttachmentId(preview.attachmentId),
2563
+ mediaType: preview.mediaType,
2564
+ width: preview.width,
2565
+ height: preview.height,
2566
+ bytes: preview.bytes,
2567
+ name: preview.name
2015
2568
  }
2016
2569
  }))];
2017
2570
  }
2018
- function validateRef(ref, parsed, input, name) {
2019
- if (typeof ref.attachmentId !== "string" || ref.attachmentId.length === 0 || ref.mediaType !== parsed.mediaType || ref.bytes !== input.data.byteLength || ref.width !== parsed.width || ref.height !== parsed.height || ref.name !== name) failure("The attachment store returned inconsistent image metadata.");
2571
+ function positiveSafeInteger$1(value) {
2572
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
2573
+ }
2574
+ function previewValue(ref, fallbackName) {
2575
+ if (typeof ref.attachmentId !== "string" || ref.attachmentId.length === 0 || ref.mediaType !== "image/png" && ref.mediaType !== "image/jpeg" && ref.mediaType !== "image/webp" || !positiveSafeInteger$1(ref.bytes) || !positiveSafeInteger$1(ref.width) || !positiveSafeInteger$1(ref.height)) failure("The attachment store returned invalid preview metadata.");
2576
+ const name = typeof ref.name === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(ref.name) ? ref.name : fallbackName;
2577
+ return {
2578
+ attachmentId: ref.attachmentId,
2579
+ mediaType: ref.mediaType,
2580
+ width: ref.width,
2581
+ height: ref.height,
2582
+ bytes: ref.bytes,
2583
+ name
2584
+ };
2020
2585
  }
2021
2586
  function executionKey(exec) {
2022
2587
  return `${String(exec.agent?.id ?? "<no-agent>")}\u0000${String(exec.rootCallId)}\u0000${String(exec.callId)}`;
2023
2588
  }
2024
- async function generate(ctx, transport, prompt, exec) {
2589
+ async function generate(ctx, transport, assets, prompt, exec) {
2025
2590
  let response;
2026
2591
  try {
2027
2592
  response = await transport.generateImages({ prompt }, { signal: exec.signal });
@@ -2057,28 +2622,49 @@ async function generate(ctx, transport, prompt, exec) {
2057
2622
  name
2058
2623
  });
2059
2624
  }
2625
+ const sessionId = exec.agent?.id;
2626
+ if (sessionId === void 0) failure("Image generation requires a session-owned tool call.");
2627
+ let originals;
2628
+ try {
2629
+ originals = await assets.saveImages(String(sessionId), inputs.map((input, index) => {
2630
+ const parsed = parsedImages[index];
2631
+ if (parsed === void 0 || input.name === void 0) failure("The generated image batch is incomplete.");
2632
+ return {
2633
+ data: input.data,
2634
+ mediaType: parsed.mediaType,
2635
+ width: parsed.width,
2636
+ height: parsed.height,
2637
+ name: input.name
2638
+ };
2639
+ }));
2640
+ } catch {
2641
+ failure("The generated original images could not be saved.");
2642
+ }
2060
2643
  let refs;
2061
2644
  try {
2062
2645
  refs = await ctx.attachments.saveImages(inputs);
2063
2646
  } catch {
2647
+ await assets.removeImages(originals);
2064
2648
  failure("The generated images could not be saved; no attachment references were returned.");
2065
2649
  }
2066
- if (refs.length !== inputs.length) failure("The attachment store returned an incomplete image batch.");
2067
- return { images: refs.map((ref, index) => {
2068
- const parsed = parsedImages[index];
2069
- const input = inputs[index];
2070
- const name = input?.name;
2071
- if (parsed === void 0 || input === void 0 || name === void 0) failure("The attachment store returned an incomplete image batch.");
2072
- validateRef(ref, parsed, input, name);
2073
- return {
2074
- attachmentId: ref.attachmentId,
2075
- mediaType: parsed.mediaType,
2076
- width: parsed.width,
2077
- height: parsed.height,
2078
- bytes: input.data.byteLength,
2079
- name
2080
- };
2081
- }) };
2650
+ if (refs.length !== inputs.length || originals.length !== inputs.length) {
2651
+ await assets.removeImages(originals);
2652
+ failure("The image stores returned an incomplete image batch.");
2653
+ }
2654
+ try {
2655
+ return { images: refs.map((ref, index) => {
2656
+ const original = originals[index];
2657
+ const name = inputs[index]?.name;
2658
+ if (original === void 0 || name === void 0) failure("The image stores returned an incomplete image batch.");
2659
+ return {
2660
+ original,
2661
+ preview: previewValue(ref, name)
2662
+ };
2663
+ }) };
2664
+ } catch (error) {
2665
+ await assets.removeImages(originals);
2666
+ throw error;
2667
+ }
2082
2668
  }
2083
2669
  function appendAbortNote(result) {
2084
2670
  if (!result.isError || result.error.info?.code !== TOOL_ABORTED) return void 0;
@@ -2089,11 +2675,11 @@ function appendAbortNote(result) {
2089
2675
  }];
2090
2676
  }
2091
2677
  /** Build one fiber-owned image tool, including in-flight call deduplication. */
2092
- function imageGenerateTool(ctx) {
2678
+ function imageGenerateTool(ctx, assets) {
2093
2679
  const inFlight = /* @__PURE__ */ new Map();
2094
2680
  return defineTool({
2095
2681
  name: IMAGE_GENERATE_TOOL_NAME,
2096
- description: "Generate an image from a text prompt and save it as a durable DSH attachment. Supports one prompt only; output size and style are service defaults.",
2682
+ description: "Generate an image from a text prompt, preserve the exact original, and save a DSH conversation preview. Supports one prompt only; output size and style are service defaults.",
2097
2683
  parameters: { prompt: {
2098
2684
  type: "string",
2099
2685
  required: true,
@@ -2110,34 +2696,81 @@ function imageGenerateTool(ctx) {
2110
2696
  type: "object",
2111
2697
  additionalProperties: false,
2112
2698
  properties: {
2113
- attachmentId: {
2114
- type: "string",
2115
- required: true
2116
- },
2117
- mediaType: {
2118
- type: "string",
2699
+ original: {
2700
+ type: "object",
2119
2701
  required: true,
2120
- enum: [
2121
- "image/png",
2122
- "image/jpeg",
2123
- "image/webp"
2124
- ]
2125
- },
2126
- width: {
2127
- type: "integer",
2128
- required: true
2129
- },
2130
- height: {
2131
- type: "integer",
2132
- required: true
2133
- },
2134
- bytes: {
2135
- type: "integer",
2136
- required: true
2702
+ additionalProperties: false,
2703
+ properties: {
2704
+ assetId: {
2705
+ type: "string",
2706
+ required: true
2707
+ },
2708
+ mediaType: {
2709
+ type: "string",
2710
+ required: true,
2711
+ enum: [
2712
+ "image/png",
2713
+ "image/jpeg",
2714
+ "image/webp"
2715
+ ]
2716
+ },
2717
+ width: {
2718
+ type: "integer",
2719
+ required: true
2720
+ },
2721
+ height: {
2722
+ type: "integer",
2723
+ required: true
2724
+ },
2725
+ bytes: {
2726
+ type: "integer",
2727
+ required: true
2728
+ },
2729
+ name: {
2730
+ type: "string",
2731
+ required: true
2732
+ },
2733
+ sha256: {
2734
+ type: "string",
2735
+ required: true
2736
+ }
2737
+ }
2137
2738
  },
2138
- name: {
2139
- type: "string",
2140
- required: true
2739
+ preview: {
2740
+ type: "object",
2741
+ required: true,
2742
+ additionalProperties: false,
2743
+ properties: {
2744
+ attachmentId: {
2745
+ type: "string",
2746
+ required: true
2747
+ },
2748
+ mediaType: {
2749
+ type: "string",
2750
+ required: true,
2751
+ enum: [
2752
+ "image/png",
2753
+ "image/jpeg",
2754
+ "image/webp"
2755
+ ]
2756
+ },
2757
+ width: {
2758
+ type: "integer",
2759
+ required: true
2760
+ },
2761
+ height: {
2762
+ type: "integer",
2763
+ required: true
2764
+ },
2765
+ bytes: {
2766
+ type: "integer",
2767
+ required: true
2768
+ },
2769
+ name: {
2770
+ type: "string",
2771
+ required: true
2772
+ }
2773
+ }
2141
2774
  }
2142
2775
  }
2143
2776
  }
@@ -2146,6 +2779,7 @@ function imageGenerateTool(ctx) {
2146
2779
  render: (_args, value) => outputContent(value),
2147
2780
  presentationMeta: (args, value) => ({
2148
2781
  kind: IMAGE_PRESENTATION_KIND,
2782
+ schemaVersion: 1,
2149
2783
  prompt: args.prompt.trim(),
2150
2784
  images: value.images
2151
2785
  })
@@ -2161,7 +2795,7 @@ function imageGenerateTool(ctx) {
2161
2795
  const key = executionKey(exec);
2162
2796
  const current = inFlight.get(key);
2163
2797
  if (current !== void 0) return current;
2164
- const pending = generate(ctx, transport, prompt, exec).catch((error) => {
2798
+ const pending = generate(ctx, transport, assets, prompt, exec).catch((error) => {
2165
2799
  if (error instanceof SafeToolError) throw error;
2166
2800
  failure(fixedTransportMessage(error));
2167
2801
  }).finally(() => {
@@ -2692,15 +3326,23 @@ function parseSuccess(bytes) {
2692
3326
  /** Core-owned Cordis service for the optional image package. */
2693
3327
  var OpenAICodexTransport = class extends Service {
2694
3328
  credentials;
3329
+ proxyManager;
3330
+ resolveProxyUrl;
2695
3331
  apiVersion = 1;
2696
3332
  models;
2697
- constructor(ctx, credentials) {
3333
+ constructor(ctx, credentials, proxyManager, resolveProxyUrl = () => void 0) {
2698
3334
  super(ctx, OPENAI_CODEX_TRANSPORT_SERVICE);
2699
3335
  this.credentials = credentials;
3336
+ this.proxyManager = proxyManager;
3337
+ this.resolveProxyUrl = resolveProxyUrl;
2700
3338
  this.models = createModels({ credentials });
2701
3339
  this.models.setProvider(openaiCodexProvider());
2702
3340
  }
2703
3341
  async generateImages(input, context) {
3342
+ const operation = () => this.generateImagesWithoutProxy(input, context);
3343
+ return this.proxyManager?.run(this.resolveProxyUrl(), operation) ?? operation();
3344
+ }
3345
+ async generateImagesWithoutProxy(input, context) {
2704
3346
  if (typeof input?.prompt !== "string" || input.prompt.trim().length === 0 || input.prompt.length > 32e3) throw new OpenAICodexTransportError(OPENAI_CODEX_TRANSPORT_ERROR_CODES.invalidRequest);
2705
3347
  if (isAborted(context.signal)) throw new OpenAICodexTransportError(OPENAI_CODEX_TRANSPORT_ERROR_CODES.canceled);
2706
3348
  if ((await this.credentials.read("openai-codex"))?.type !== "oauth") throw new OpenAICodexTransportError(OPENAI_CODEX_TRANSPORT_ERROR_CODES.signedOut);
@@ -2771,67 +3413,168 @@ var OpenAICodexTransport = class extends Service {
2771
3413
  }
2772
3414
  };
2773
3415
  //#endregion
2774
- //#region src/settings-contract.ts
2775
- /** Node-free settings contract shared by the Host plugin and browser card. */
2776
- /** Stable Harness settings namespace owned by this plugin. */
2777
- const OPENAI_CODEX_SETTINGS_NAMESPACE = "llm-openai-codex";
2778
- /** Default model used by the standalone search endpoint. */
2779
- const DEFAULT_OPENAI_CODEX_SEARCH_MODEL = "gpt-5.6-sol";
2780
- /** Default search mode, matching the official local Codex client. */
2781
- const DEFAULT_OPENAI_CODEX_SEARCH_MODE = "cached";
2782
- /** Default provider search-context size. */
2783
- const DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE = "medium";
2784
- /** Default output budget for the standalone search response. */
2785
- const DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS = 1e4;
2786
- const DEFAULT_OPENAI_CODEX_SETTINGS = Object.freeze({
2787
- models: void 0,
2788
- enableSearch: false,
2789
- enableImageTool: false,
2790
- enableImageGeneration: false,
2791
- searchModel: DEFAULT_OPENAI_CODEX_SEARCH_MODEL,
2792
- searchMode: DEFAULT_OPENAI_CODEX_SEARCH_MODE,
2793
- searchContextSize: DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE,
2794
- searchMaxOutputTokens: DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS
2795
- });
2796
- /** Fill the schema defaults even when called without Cordis validation. */
2797
- function resolveOpenAICodexSettings(value) {
2798
- return {
2799
- ...DEFAULT_OPENAI_CODEX_SETTINGS,
2800
- ...value
3416
+ //#region src/image-assets.ts
3417
+ /** Owner-only storage for exact GPT Image output bytes. */
3418
+ /** Versioned plugin-owned root below DSH_HOME; this is not the DSH attachment store. */
3419
+ const OPENAI_CODEX_IMAGE_ASSET_DIRECTORY = "dsh-codex-connect/images/v1";
3420
+ const METADATA_VERSION = 1;
3421
+ const ORIGINAL_FILENAME = "original";
3422
+ const METADATA_FILENAME = "metadata.json";
3423
+ const SAFE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
3424
+ function digest(data) {
3425
+ return createHash("sha256").update(data).digest("hex");
3426
+ }
3427
+ function positiveSafeInteger(value) {
3428
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
3429
+ }
3430
+ function mediaType(value) {
3431
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp";
3432
+ }
3433
+ function validSessionId(value) {
3434
+ return typeof value === "string" && value.length > 0 && value.length <= 512;
3435
+ }
3436
+ function parseDocument(text) {
3437
+ let value;
3438
+ try {
3439
+ value = JSON.parse(text);
3440
+ } catch {
3441
+ return;
3442
+ }
3443
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
3444
+ const candidate = value;
3445
+ if (candidate.version !== METADATA_VERSION || !validSessionId(candidate.sessionId)) return void 0;
3446
+ const image = decodeOpenAICodexOriginalImageRef(candidate.image);
3447
+ return image === void 0 ? void 0 : {
3448
+ version: METADATA_VERSION,
3449
+ sessionId: candidate.sessionId,
3450
+ image
2801
3451
  };
2802
3452
  }
2803
- function isRecord$1(value) {
2804
- return typeof value === "object" && value !== null && !Array.isArray(value);
3453
+ async function assertOwnerFile(filename) {
3454
+ const metadata = await lstat(filename);
3455
+ if (!metadata.isFile()) throw new Error("original image object is not a regular file");
3456
+ /* v8 ignore next -- native Windows does not expose POSIX permission semantics. */
3457
+ if (process.platform !== "win32" && (metadata.mode & 63) !== 0) throw new Error("original image object is readable beyond its owner");
2805
3458
  }
2806
- /** Narrow the redacted settings wire payload before it enters React state. */
2807
- function decodeOpenAICodexSettings(value) {
2808
- if (!isRecord$1(value)) return void 0;
2809
- const models = value["models"];
2810
- const enableSearch = value["enableSearch"];
2811
- const enableImageTool = value["enableImageTool"];
2812
- const enableImageGeneration = value["enableImageGeneration"];
2813
- const searchModel = value["searchModel"];
2814
- const searchMode = value["searchMode"];
2815
- const searchContextSize = value["searchContextSize"];
2816
- const searchMaxOutputTokens = value["searchMaxOutputTokens"];
2817
- if (models !== void 0 && (!Array.isArray(models) || models.some((model) => typeof model !== "string"))) return void 0;
2818
- if (typeof enableSearch !== "boolean" || typeof enableImageTool !== "boolean") return void 0;
2819
- if (enableImageGeneration !== void 0 && typeof enableImageGeneration !== "boolean") return void 0;
2820
- if (typeof searchModel !== "string" || searchModel.trim().length === 0) return void 0;
2821
- if (searchMode !== "cached" && searchMode !== "indexed" && searchMode !== "live") return void 0;
2822
- if (searchContextSize !== "low" && searchContextSize !== "medium" && searchContextSize !== "high") return void 0;
2823
- if (typeof searchMaxOutputTokens !== "number" || !Number.isInteger(searchMaxOutputTokens) || searchMaxOutputTokens < 1) return void 0;
2824
- return {
2825
- models: models === void 0 ? void 0 : [...new Set(models)],
2826
- enableSearch,
2827
- enableImageTool,
2828
- enableImageGeneration: enableImageGeneration ?? false,
2829
- searchModel,
2830
- searchMode,
2831
- searchContextSize,
2832
- searchMaxOutputTokens
2833
- };
3459
+ async function writeBinaryFile(filename, data) {
3460
+ await mkdir(dirname(filename), {
3461
+ recursive: true,
3462
+ mode: 448
3463
+ });
3464
+ const handle = await open(filename, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
3465
+ try {
3466
+ await handle.writeFile(data);
3467
+ await handle.sync();
3468
+ } finally {
3469
+ await handle.close();
3470
+ }
2834
3471
  }
3472
+ /** Exact-byte image store kept separate from DSH's deliberately normalized attachments. */
3473
+ var OpenAICodexImageAssetStore = class {
3474
+ root;
3475
+ constructor(dshHome) {
3476
+ this.root = resolve(join(resolveDshHome(dshHome), OPENAI_CODEX_IMAGE_ASSET_DIRECTORY));
3477
+ }
3478
+ directory(assetId) {
3479
+ if (!OPENAI_CODEX_IMAGE_ASSET_ID_PATTERN.test(assetId)) throw new TypeError("invalid original image asset id");
3480
+ return join(this.root, assetId.slice(4, 6), assetId);
3481
+ }
3482
+ async saveOne(sessionId, input) {
3483
+ if (!validSessionId(sessionId)) throw new TypeError("invalid original image session id");
3484
+ if (!(input.data instanceof Uint8Array) || input.data.byteLength < 1 || input.data.byteLength > 50331648 || !mediaType(input.mediaType) || !positiveSafeInteger(input.width) || !positiveSafeInteger(input.height) || !SAFE_NAME_PATTERN.test(input.name)) throw new TypeError("invalid original image input");
3485
+ const detected = detectEncodedImage(input.data);
3486
+ if (detected === void 0 || detected.mediaType !== input.mediaType || detected.width !== input.width || detected.height !== input.height) throw new TypeError("original image bytes do not match their declared metadata");
3487
+ const assetId = `img_${randomUUID().replaceAll("-", "")}`;
3488
+ const directory = this.directory(assetId);
3489
+ const ref = {
3490
+ assetId,
3491
+ mediaType: input.mediaType,
3492
+ width: input.width,
3493
+ height: input.height,
3494
+ bytes: input.data.byteLength,
3495
+ name: input.name,
3496
+ sha256: digest(input.data)
3497
+ };
3498
+ try {
3499
+ await mkdir(dirname(directory), {
3500
+ recursive: true,
3501
+ mode: 448
3502
+ });
3503
+ await mkdir(directory, {
3504
+ recursive: false,
3505
+ mode: 448
3506
+ });
3507
+ await writeBinaryFile(join(directory, ORIGINAL_FILENAME), input.data);
3508
+ const document = {
3509
+ version: METADATA_VERSION,
3510
+ sessionId,
3511
+ image: ref
3512
+ };
3513
+ await writeFileAtomic(join(directory, METADATA_FILENAME), `${JSON.stringify(document, null, 2)}\n`, {
3514
+ mode: 384,
3515
+ dirMode: 448
3516
+ });
3517
+ return ref;
3518
+ } catch (error) {
3519
+ await rm(directory, {
3520
+ recursive: true,
3521
+ force: true
3522
+ }).catch(() => void 0);
3523
+ throw error;
3524
+ }
3525
+ }
3526
+ /** Save one complete response batch and remove already-written members if a later write fails. */
3527
+ async saveImages(sessionId, inputs) {
3528
+ if (inputs.length < 1 || inputs.length > 4) throw new TypeError("original image batch must contain 1 to 4 images");
3529
+ const refs = [];
3530
+ try {
3531
+ for (const input of inputs) refs.push(await this.saveOne(sessionId, input));
3532
+ return refs;
3533
+ } catch (error) {
3534
+ await this.removeImages(refs);
3535
+ throw error;
3536
+ }
3537
+ }
3538
+ /** Remove exact assets created by a failed cross-store operation. */
3539
+ async removeImages(refs) {
3540
+ await Promise.all(refs.map(async (ref) => {
3541
+ try {
3542
+ await rm(this.directory(ref.assetId), {
3543
+ recursive: true,
3544
+ force: true
3545
+ });
3546
+ } catch {}
3547
+ }));
3548
+ }
3549
+ /**
3550
+ * Read verified bytes for the owner or a server-authorized inherited reference.
3551
+ * @param sessionId - requesting session.
3552
+ * @param assetId - opaque original identifier.
3553
+ * @param inherited - reference resolved from the session's immutable fork prefix, never request data.
3554
+ * @returns the exact original, or undefined for denied access or invalid files.
3555
+ */
3556
+ async read(sessionId, assetId, inherited) {
3557
+ if (!validSessionId(sessionId) || !OPENAI_CODEX_IMAGE_ASSET_ID_PATTERN.test(assetId)) return void 0;
3558
+ const directory = this.directory(assetId);
3559
+ const metadataPath = join(directory, METADATA_FILENAME);
3560
+ const originalPath = join(directory, ORIGINAL_FILENAME);
3561
+ try {
3562
+ await Promise.all([assertOwnerFile(metadataPath), assertOwnerFile(originalPath)]);
3563
+ const document = parseDocument(await readFile(metadataPath, "utf8"));
3564
+ if (document === void 0 || document.image.assetId !== assetId) return void 0;
3565
+ if (document.sessionId !== sessionId && (inherited === void 0 || inherited.assetId !== document.image.assetId || inherited.sha256 !== document.image.sha256 || inherited.mediaType !== document.image.mediaType || inherited.width !== document.image.width || inherited.height !== document.image.height || inherited.bytes !== document.image.bytes || inherited.name !== document.image.name)) return void 0;
3566
+ const data = new Uint8Array(await readFile(originalPath));
3567
+ const detected = detectEncodedImage(data);
3568
+ if (data.byteLength !== document.image.bytes || digest(data) !== document.image.sha256 || detected === void 0 || detected.mediaType !== document.image.mediaType || detected.width !== document.image.width || detected.height !== document.image.height) return void 0;
3569
+ return {
3570
+ ref: document.image,
3571
+ data
3572
+ };
3573
+ } catch {
3574
+ return;
3575
+ }
3576
+ }
3577
+ };
2835
3578
  //#endregion
2836
3579
  //#region src/search.ts
2837
3580
  /**
@@ -2974,6 +3717,10 @@ var OpenAICodexSearchProvider = class {
2974
3717
  }
2975
3718
  /** @inheritdoc */
2976
3719
  async search(request, signal) {
3720
+ const operation = () => this.searchWithoutProxy(request, signal);
3721
+ return this.options.proxyManager?.run(this.options.resolveProxyUrl?.(), operation) ?? operation();
3722
+ }
3723
+ async searchWithoutProxy(request, signal) {
2977
3724
  throwIfSearchAborted(signal);
2978
3725
  let auth;
2979
3726
  try {
@@ -3319,6 +4066,8 @@ const inject = ["llm"];
3319
4066
  const OPENAI_CODEX_SETTINGS_NS = settingsNamespace(OPENAI_CODEX_SETTINGS_NAMESPACE);
3320
4067
  const Config = z.object({
3321
4068
  models: z.union([z.const(void 0), z.array(z.string())]),
4069
+ enableProxy: z.boolean().default(false),
4070
+ proxyUrl: z.string().default(DEFAULT_OPENAI_CODEX_PROXY_URL),
3322
4071
  enableSearch: z.boolean().default(false),
3323
4072
  enableImageTool: z.boolean().default(false),
3324
4073
  enableImageGeneration: z.boolean().default(false),
@@ -3344,16 +4093,22 @@ const Config = z.object({
3344
4093
  */
3345
4094
  function apply(ctx, config) {
3346
4095
  let current = () => config;
4096
+ const proxyManager = new OpenAICodexProxyManager();
4097
+ const resolveProviderProxyUrl = () => resolveOpenAICodexProxyUrl(resolveOpenAICodexSettings(current()));
4098
+ let proxyWasActive = resolveProviderProxyUrl() !== void 0;
3347
4099
  const credentials = new OpenAICodexCredentialStore();
4100
+ const imageAssets = new OpenAICodexImageAssetStore();
3348
4101
  const trustedOrigins = new OpenAICodexTrustedOriginsStore(join(dirname(credentials.filename), OPENAI_CODEX_TRUSTED_ORIGINS_FILENAME));
3349
4102
  const fastMode = new FastModeRegistry();
3350
4103
  assertNoOpenAICodexProviderConflict(ctx.llm.listProviders().map((provider) => provider.id));
3351
- new OpenAICodexTransport(ctx, credentials);
3352
- ctx.llm.registerAdapter([OPENAI_CODEX_PROVIDER], createOpenAICodexAdapter(credentials, () => ctx.get("attachments"), fastMode, () => resolveOpenAICodexSettings(current()).models));
4104
+ new OpenAICodexTransport(ctx, credentials, proxyManager, resolveProviderProxyUrl);
4105
+ ctx.llm.registerAdapter([OPENAI_CODEX_PROVIDER], createOpenAICodexAdapter(credentials, () => ctx.get("attachments"), fastMode, () => resolveOpenAICodexSettings(current()).models, proxyManager, resolveProviderProxyUrl));
3353
4106
  ctx.inject(["webServer"], (webCtx) => {
3354
- registerOpenAICodexAuthRoutes(webCtx, credentials, trustedOrigins, fastMode);
4107
+ registerOpenAICodexAuthRoutes(webCtx, credentials, trustedOrigins, fastMode, proxyManager, resolveProviderProxyUrl);
4108
+ registerOpenAICodexProxyRoutes(webCtx, trustedOrigins, proxyManager);
3355
4109
  registerOpenAICodexUpdateRoutes(webCtx, { currentVersion: CODEX_CONNECT_VERSION }, trustedOrigins);
3356
4110
  registerOpenAICodexModelCatalogRoute(webCtx, openAICodexModelCatalog, trustedOrigins);
4111
+ registerOpenAICodexOriginalImageRoute(webCtx, trustedOrigins, imageAssets);
3357
4112
  });
3358
4113
  let stopped = false;
3359
4114
  let searchFiber;
@@ -3384,7 +4139,9 @@ function apply(ctx, config) {
3384
4139
  mode: nextRegistration.mode,
3385
4140
  contextSize: nextRegistration.contextSize,
3386
4141
  maxOutputTokens: nextRegistration.maxOutputTokens,
3387
- resolveRequestId: () => String(webCtx.get("agents")?.currentInitiator()?.session.id ?? randomUUID())
4142
+ resolveRequestId: () => String(webCtx.get("agents")?.currentInitiator()?.session.id ?? randomUUID()),
4143
+ proxyManager,
4144
+ resolveProxyUrl: resolveProviderProxyUrl
3388
4145
  })));
3389
4146
  searchFiber = fiber;
3390
4147
  searchRegistration = nextRegistration;
@@ -3425,7 +4182,7 @@ function apply(ctx, config) {
3425
4182
  imageGenerationFiber = void 0;
3426
4183
  if (previous !== void 0) await previous.dispose();
3427
4184
  if (stopped || !enabled) return;
3428
- const fiber = ctx.inject(["tools", "attachments"], (toolCtx) => toolCtx.tools.register(imageGenerateTool(toolCtx)));
4185
+ const fiber = ctx.inject(["tools", "attachments"], (toolCtx) => toolCtx.tools.register(imageGenerateTool(toolCtx, imageAssets)));
3429
4186
  imageGenerationFiber = fiber;
3430
4187
  Promise.resolve(fiber).catch((error) => {
3431
4188
  if (imageGenerationFiber === fiber) imageGenerationFiber = void 0;
@@ -3465,14 +4222,27 @@ function apply(ctx, config) {
3465
4222
  image?.dispose() ?? Promise.resolve(),
3466
4223
  imageGeneration?.dispose() ?? Promise.resolve()
3467
4224
  ]);
4225
+ await proxyManager.dispose();
3468
4226
  }, "dsh-codex-connect: optional capability lifecycle");
3469
4227
  installSettingsSection(ctx, OPENAI_CODEX_SETTINGS_NS, Config, config, {
4228
+ validate(value) {
4229
+ resolveOpenAICodexSettings(value);
4230
+ if (value.enableProxy === true && !isValidOpenAICodexProxyUrl(value.proxyUrl)) throw new TypeError("OpenAI Codex proxyUrl must be an HTTP(S) origin without credentials or a path");
4231
+ },
3470
4232
  setSource(source) {
3471
4233
  current = source;
3472
4234
  },
3473
- onChange: scheduleCapabilities
4235
+ onChange() {
4236
+ const proxyIsActive = resolveProviderProxyUrl() !== void 0;
4237
+ if (proxyWasActive && !proxyIsActive) proxyManager.deactivate().catch((error) => {
4238
+ ctx.logger.error("dsh-codex-connect: could not deactivate the provider proxy");
4239
+ ctx.logger.error(error);
4240
+ });
4241
+ proxyWasActive = proxyIsActive;
4242
+ scheduleCapabilities();
4243
+ }
3474
4244
  });
3475
4245
  scheduleCapabilities();
3476
4246
  }
3477
4247
  //#endregion
3478
- export { assessCompatibility as $, OPENAI_CODEX_TRANSPORT_SERVICE as A, OPENAI_CODEX_UPDATE_PATH as B, OPENAI_CODEX_IMAGE_MAX_COUNT as C, OPENAI_CODEX_IMAGE_REQUEST_TIMEOUT_MS as D, OPENAI_CODEX_IMAGE_PROMPT_MAX_LENGTH as E, IMAGE_GENERATE_TOOL_NAME as F, COMPATIBILITY_CONTRACT as G, compareOpenAICodexVersions as H, assertNoOpenAICodexProviderConflict as I, DSH_PLUGIN_API_PACKAGES as J, COMPATIBILITY_PACKAGES as K, diagnoseOpenAICodex as L, OpenAICodexTransportError as M, isOpenAICodexTransportError as N, OPENAI_CODEX_TRANSPORT_API_VERSION as O, VIEW_IMAGE_TOOL_NAME as P, SUPPORTED_PI_AI_VERSION as Q, openAICodexConflictMessage as R, OPENAI_CODEX_IMAGE_GENERATION_URL as S, OPENAI_CODEX_IMAGE_MAX_RESPONSE_BYTES as T, parseOpenAICodexUpdateResult as U, checkForOpenAICodexUpdate as V, parseOpenAICodexVersion as W, SUPPORTED_DSH_PLUGIN_API_VERSION as X, PI_AI_PACKAGE as Y, SUPPORTED_NODE_RANGE as Z, DEFAULT_OPENAI_CODEX_SEARCH_MODEL as _, OpenAICodexCredentialStore as _t, name as a, OPENAI_CODEX_FAST_MODE_MAX_SESSION_ID_LENGTH as at, decodeOpenAICodexSettings as b, migrateOpenAICodexSearchHistory as c, normalizeTrustedOrigin as ct, OPENAI_CODEX_SEARCH_URL as d, readOpenAICodexRateLimits as dt, detectCompatibility as et, OpenAICodexSearchProvider as f, loginOpenAICodex as ft, DEFAULT_OPENAI_CODEX_SEARCH_MODE as g, OPENAI_CODEX_PROVIDER as gt, DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS as h, OPENAI_CODEX_AUTH_FILENAME as ht, inject as i, OPENAI_CODEX_FAST_MODE_MAX_SESSIONS as it, OpenAICodexTransport as j, OPENAI_CODEX_TRANSPORT_ERROR_CODES as k, OPENAI_CODEX_BASE_URL as l, OPENAI_CODEX_USAGE_URL as lt, DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE as m, openAICodexAuthStatus as mt, OPENAI_CODEX_SETTINGS_NS as n, OPENAI_CODEX_FAST_MODE_PATH as nt, OPENAI_CODEX_HISTORY_BACKUP_SUFFIX as o, isFastModeSessionId as ot, mapOpenAICodexSearchResponse as p, logoutOpenAICodex as pt, COMPATIBILITY_SCHEMA_VERSION as q, apply as r, FastModeRegistry as rt, OPENAI_CODEX_SEARCH_MODEL_REQUEST_EVENT as s, OpenAICodexTrustedOriginsStore as st, Config as t, evaluateCompatibility as tt, OPENAI_CODEX_SEARCH_PROVIDER as u, parseOpenAICodexUsage as ut, DEFAULT_OPENAI_CODEX_SETTINGS as v, openAICodexAuthPath as vt, OPENAI_CODEX_IMAGE_MAX_ERROR_BYTES as w, resolveOpenAICodexSettings as x, OPENAI_CODEX_SETTINGS_NAMESPACE as y, CODEX_CONNECT_VERSION as z };
4248
+ export { OPENAI_CODEX_PROXY_PROBE_TIMEOUT_MS as $, openAICodexConflictMessage as A, OPENAI_CODEX_AUTH_FILENAME as At, DSH_PLUGIN_API_PACKAGES as B, OpenAICodexTransport as C, OPENAI_CODEX_USAGE_URL as Ct, IMAGE_GENERATE_TOOL_NAME as D, logoutOpenAICodex as Dt, VIEW_IMAGE_TOOL_NAME as E, loginOpenAICodex as Et, parseOpenAICodexUpdateResult as F, assessCompatibility as G, SUPPORTED_DSH_PLUGIN_API_VERSION as H, parseOpenAICodexVersion as I, readInstalledPackageVersion as J, detectCompatibility as K, COMPATIBILITY_CONTRACT as L, OPENAI_CODEX_UPDATE_PATH as M, OpenAICodexCredentialStore as Mt, checkForOpenAICodexUpdate as N, openAICodexAuthPath as Nt, assertNoOpenAICodexProviderConflict as O, openAICodexAuthStatus as Ot, compareOpenAICodexVersions as P, OPENAI_CODEX_PROXY_CANDIDATE_LIMIT as Q, COMPATIBILITY_PACKAGES as R, OPENAI_CODEX_TRANSPORT_SERVICE as S, normalizeTrustedOrigin as St, isOpenAICodexTransportError as T, readOpenAICodexRateLimits as Tt, SUPPORTED_NODE_RANGE as U, PI_AI_PACKAGE as V, SUPPORTED_PI_AI_VERSION as W, OPENAI_CODEX_PROXY_TEST_PATH as X, OPENAI_CODEX_PROXY_DETECT_PATH as Y, OPENAI_CODEX_LOCAL_PROXY_CANDIDATES as Z, OPENAI_CODEX_IMAGE_MAX_RESPONSE_BYTES as _, FastModeRegistry as _t, name as a, DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE as at, OPENAI_CODEX_TRANSPORT_API_VERSION as b, isFastModeSessionId as bt, migrateOpenAICodexSearchHistory as c, DEFAULT_OPENAI_CODEX_SEARCH_MODEL as ct, OPENAI_CODEX_SEARCH_URL as d, decodeOpenAICodexSettings as dt, OPENAI_CODEX_PROXY_PROBE_URL as et, OpenAICodexSearchProvider as f, isValidOpenAICodexProxyUrl as ft, OPENAI_CODEX_IMAGE_MAX_ERROR_BYTES as g, OPENAI_CODEX_FAST_MODE_PATH as gt, OPENAI_CODEX_IMAGE_MAX_COUNT as h, resolveOpenAICodexSettings as ht, inject as i, DEFAULT_OPENAI_CODEX_PROXY_URL as it, CODEX_CONNECT_VERSION as j, OPENAI_CODEX_PROVIDER as jt, diagnoseOpenAICodex as k, openAICodexModelCatalog as kt, OPENAI_CODEX_BASE_URL as l, DEFAULT_OPENAI_CODEX_SETTINGS as lt, OPENAI_CODEX_IMAGE_GENERATION_URL as m, resolveOpenAICodexProxyUrl as mt, OPENAI_CODEX_SETTINGS_NS as n, detectOpenAICodexProxies as nt, OPENAI_CODEX_HISTORY_BACKUP_SUFFIX as o, DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS as ot, mapOpenAICodexSearchResponse as p, normalizeOpenAICodexProxyUrl as pt, evaluateCompatibility as q, apply as r, listOpenAICodexProxyCandidates as rt, OPENAI_CODEX_SEARCH_MODEL_REQUEST_EVENT as s, DEFAULT_OPENAI_CODEX_SEARCH_MODE as st, Config as t, OpenAICodexProxyManager as tt, OPENAI_CODEX_SEARCH_PROVIDER as u, OPENAI_CODEX_SETTINGS_NAMESPACE as ut, OPENAI_CODEX_IMAGE_PROMPT_MAX_LENGTH as v, OPENAI_CODEX_FAST_MODE_MAX_SESSIONS as vt, OpenAICodexTransportError as w, parseOpenAICodexUsage as wt, OPENAI_CODEX_TRANSPORT_ERROR_CODES as x, OpenAICodexTrustedOriginsStore as xt, OPENAI_CODEX_IMAGE_REQUEST_TIMEOUT_MS as y, OPENAI_CODEX_FAST_MODE_MAX_SESSION_ID_LENGTH as yt, COMPATIBILITY_SCHEMA_VERSION as z };