dsh-codex-connect 0.1.0-alpha.4.3

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.
@@ -0,0 +1,1484 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { deepEqualJson, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
4
+ import { createModels } from "@earendil-works/pi-ai";
5
+ import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
6
+ import { createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
7
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
8
+ import { lstat, mkdir, readFile, rm, stat } from "node:fs/promises";
9
+ import { basename, dirname, join, resolve } from "node:path";
10
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
11
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
12
+ import { AttachmentId } from "@deepseek-ai/dsh-attachment";
13
+ import { defineTool } from "@deepseek-ai/dsh-tools";
14
+ import { lookup } from "node:dns/promises";
15
+ import { request } from "node:http";
16
+ import { request as request$1 } from "node:https";
17
+ import { BlockList, isIP } from "node:net";
18
+ import { KNOWN_SESSION_EVENT_TYPES } from "@deepseek-ai/dsh-session";
19
+ import { WebError } from "@deepseek-ai/dsh-web";
20
+ //#region src/store.ts
21
+ /**
22
+ * Owner-only persistent OAuth credential storage for the OpenAI Codex bundle.
23
+ * @module dsh-codex-connect/store
24
+ */
25
+ /** Provider route and pi-ai provider id owned by this bundle. */
26
+ const OPENAI_CODEX_PROVIDER = "openai-codex";
27
+ /** Basename of the OAuth document inside the Harness home. */
28
+ const OPENAI_CODEX_AUTH_FILENAME = ".openai-codex-auth.json";
29
+ /** Current on-disk format; pre-release readers reject every other version. */
30
+ const AUTH_FORMAT_VERSION = 1;
31
+ /** Whether a filesystem error reports an absent path. */
32
+ function isENOENT(error) {
33
+ return error?.code === "ENOENT";
34
+ }
35
+ /** Reject a credential document readable by another POSIX user. */
36
+ async function assertOwnerOnly(filename) {
37
+ let mode;
38
+ try {
39
+ mode = (await stat(filename)).mode;
40
+ } catch (error) {
41
+ if (isENOENT(error)) return;
42
+ throw error;
43
+ }
44
+ /* v8 ignore next -- native Windows coverage takes the mode-less branch */
45
+ if (process.platform === "win32") return;
46
+ /* v8 ignore start -- POSIX tests cover this branch; Windows cannot express it */
47
+ if ((mode & 63) !== 0) throw new Error(`openai-codex: ${filename} is readable beyond its owner (mode ${(mode & 511).toString(8)}); run "chmod 600 ${filename}" before starting again`);
48
+ /* v8 ignore stop */
49
+ }
50
+ /** Validate the strict JSON document without quoting token-bearing input. */
51
+ function parseDocument(text, filename) {
52
+ let value;
53
+ try {
54
+ value = JSON.parse(text);
55
+ } catch {
56
+ throw new Error(`openai-codex: ${filename} is not valid JSON`);
57
+ }
58
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`openai-codex: ${filename} must contain an object`);
59
+ const document = value;
60
+ if (document["version"] !== AUTH_FORMAT_VERSION) throw new Error(`openai-codex: ${filename} has unsupported auth format version ${String(document["version"])}`);
61
+ if (Object.keys(document).some((key) => key !== "version" && key !== "credential")) throw new Error(`openai-codex: ${filename} contains an unknown top-level field`);
62
+ const raw = document["credential"];
63
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`openai-codex: ${filename} credential must be an object`);
64
+ const credential = raw;
65
+ if (Object.keys(credential).some((key) => ![
66
+ "type",
67
+ "access",
68
+ "refresh",
69
+ "expires",
70
+ "accountId"
71
+ ].includes(key))) throw new Error(`openai-codex: ${filename} credential contains an unknown field`);
72
+ if (credential["type"] !== "oauth") throw new Error(`openai-codex: ${filename} credential type must be oauth`);
73
+ for (const key of [
74
+ "access",
75
+ "refresh",
76
+ "accountId"
77
+ ]) if (typeof credential[key] !== "string" || credential[key].length === 0) throw new Error(`openai-codex: ${filename} credential ${key} must be a non-empty string`);
78
+ if (typeof credential["expires"] !== "number" || !Number.isFinite(credential["expires"]) || credential["expires"] <= 0) throw new Error(`openai-codex: ${filename} credential expires must be a positive finite number`);
79
+ return {
80
+ version: AUTH_FORMAT_VERSION,
81
+ credential
82
+ };
83
+ }
84
+ /** Detach a credential from callers that may mutate provider-owned extras. */
85
+ function cloneCredential(credential) {
86
+ return structuredClone(credential);
87
+ }
88
+ /**
89
+ * Resolve the default OAuth document path.
90
+ * @param dshHome - optional Harness-home override.
91
+ * @returns the absolute owner-only document path.
92
+ */
93
+ function openAICodexAuthPath(dshHome) {
94
+ return resolve(join(resolveDshHome(dshHome), OPENAI_CODEX_AUTH_FILENAME));
95
+ }
96
+ /** File-backed pi-ai store scoped to the single OpenAI Codex provider. */
97
+ var OpenAICodexCredentialStore = class {
98
+ /** Absolute credential document path. */
99
+ filename;
100
+ /**
101
+ * @param filename - explicit document path, defaulting under `$DSH_HOME`.
102
+ */
103
+ constructor(filename = openAICodexAuthPath()) {
104
+ this.filename = resolve(filename);
105
+ }
106
+ /** Read and validate the current document without acquiring the writer lock. */
107
+ async readCurrent() {
108
+ await assertOwnerOnly(this.filename);
109
+ let text;
110
+ try {
111
+ text = await readFile(this.filename, "utf8");
112
+ } catch (error) {
113
+ if (isENOENT(error)) return void 0;
114
+ throw error;
115
+ }
116
+ return cloneCredential(parseDocument(text, this.filename).credential);
117
+ }
118
+ /** @inheritdoc */
119
+ async read(providerId) {
120
+ return providerId === "openai-codex" ? this.readCurrent() : void 0;
121
+ }
122
+ /** @inheritdoc */
123
+ async list() {
124
+ return await this.readCurrent() === void 0 ? [] : [{
125
+ providerId: OPENAI_CODEX_PROVIDER,
126
+ type: "oauth"
127
+ }];
128
+ }
129
+ /** @inheritdoc */
130
+ async modify(providerId, fn) {
131
+ if (providerId !== "openai-codex") throw new Error(`openai-codex: credential store does not own provider "${providerId}"`);
132
+ await mkdir(dirname(this.filename), {
133
+ recursive: true,
134
+ mode: 448
135
+ });
136
+ return withFileLock(this.filename, async () => {
137
+ const current = await this.readCurrent();
138
+ const candidate = await fn(current);
139
+ if (candidate === void 0) return current;
140
+ const document = parseDocument(JSON.stringify({
141
+ version: AUTH_FORMAT_VERSION,
142
+ credential: candidate
143
+ }), this.filename);
144
+ await writeFileAtomic(this.filename, `${JSON.stringify(document, null, 2)}\n`, {
145
+ mode: 384,
146
+ dirMode: 448
147
+ });
148
+ return cloneCredential(document.credential);
149
+ });
150
+ }
151
+ /** @inheritdoc */
152
+ async delete(providerId) {
153
+ if (providerId !== "openai-codex") return;
154
+ await mkdir(dirname(this.filename), {
155
+ recursive: true,
156
+ mode: 448
157
+ });
158
+ await withFileLock(this.filename, () => rm(this.filename, { force: true }));
159
+ }
160
+ };
161
+ //#endregion
162
+ //#region src/adapter.ts
163
+ /** OpenAI Codex adapter assembled from public dsh-llm-pi-ai extension points. */
164
+ /** Provider idle ceiling used by the composite route. */
165
+ const OPENAI_CODEX_STREAM_IDLE_TIMEOUT_MS = 3e5;
166
+ /**
167
+ * Give the generic dsh adapter a request-scoped bearer-token entry without
168
+ * changing the provider's user-facing OAuth flow. The resolver accepts only
169
+ * the explicit override supplied by this plugin; it never discovers an API
170
+ * key from the environment or persistent api-key credentials.
171
+ */
172
+ function requestProvider(provider) {
173
+ return {
174
+ ...provider,
175
+ auth: {
176
+ ...provider.auth,
177
+ apiKey: {
178
+ name: "OpenAI Codex OAuth bearer token",
179
+ async resolve({ credential }) {
180
+ const apiKey = credential?.key;
181
+ return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
182
+ auth: { apiKey },
183
+ source: "OAuth"
184
+ };
185
+ }
186
+ }
187
+ }
188
+ };
189
+ }
190
+ /**
191
+ * Create the Codex subscription adapter without requiring a dsh fork. The
192
+ * public pi-ai adapter owns Harness message conversion, image attachment
193
+ * resolution, streaming, reasoning metadata, and compaction behavior; this
194
+ * plugin supplies its provider-native OAuth token for each request.
195
+ */
196
+ function createOpenAICodexAdapter(credentials, resolveAttachments) {
197
+ const provider = openaiCodexProvider();
198
+ const profiles = /* @__PURE__ */ new Map([[OPENAI_CODEX_PROVIDER, {
199
+ provider: OPENAI_CODEX_PROVIDER,
200
+ displayName: "OpenAI Codex",
201
+ streamIdleTimeoutMs: OPENAI_CODEX_STREAM_IDLE_TIMEOUT_MS,
202
+ retryPolicy: resolveRetryPolicy(void 0, "dsh-codex-connect retryPolicy"),
203
+ configuredMaxTokens: /* @__PURE__ */ new Map(),
204
+ piProvider: requestProvider(provider)
205
+ }]]);
206
+ const models = createModels({ credentials });
207
+ models.setProvider(provider);
208
+ return new PiAiAdapter({
209
+ profiles: () => profiles,
210
+ resolveApiKey: async () => (await models.getAuth(OPENAI_CODEX_PROVIDER))?.auth.apiKey,
211
+ resolveAttachments
212
+ });
213
+ }
214
+ //#endregion
215
+ //#region src/auth.ts
216
+ /**
217
+ * OpenAI Codex OAuth orchestration shared by the plugin and standalone launcher.
218
+ * @module dsh-codex-connect/auth
219
+ */
220
+ /**
221
+ * Complete provider-native OAuth and persist the resulting credential.
222
+ * @param interaction - terminal or UI callbacks for the provider flow.
223
+ * @param store - credential store, defaulting under `$DSH_HOME`.
224
+ */
225
+ async function loginOpenAICodex(interaction, store = new OpenAICodexCredentialStore()) {
226
+ const models = createModels({ credentials: store });
227
+ models.setProvider(openaiCodexProvider());
228
+ await models.login(OPENAI_CODEX_PROVIDER, "oauth", interaction);
229
+ }
230
+ /**
231
+ * Remove the stored OpenAI Codex credential.
232
+ * @param store - credential store, defaulting under `$DSH_HOME`.
233
+ */
234
+ async function logoutOpenAICodex(store = new OpenAICodexCredentialStore()) {
235
+ await store.delete(OPENAI_CODEX_PROVIDER);
236
+ }
237
+ /**
238
+ * Read non-secret OpenAI Codex login state without refreshing the token.
239
+ * @param store - credential store, defaulting under `$DSH_HOME`.
240
+ * @returns stored login state and expiry.
241
+ */
242
+ async function openAICodexAuthStatus(store = new OpenAICodexCredentialStore()) {
243
+ const credential = await store.read(OPENAI_CODEX_PROVIDER);
244
+ return credential?.type === "oauth" ? {
245
+ authenticated: true,
246
+ expiresAt: new Date(credential.expires)
247
+ } : { authenticated: false };
248
+ }
249
+ //#endregion
250
+ //#region src/usage.ts
251
+ /** Live ChatGPT Codex rate-limit usage for the browser account page. */
252
+ /** Fixed endpoint used by the official Codex client for ChatGPT rate limits. */
253
+ const OPENAI_CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
254
+ const USAGE_REQUEST_TIMEOUT_MS = 15e3;
255
+ function isRecord$2(value) {
256
+ return typeof value === "object" && value !== null && !Array.isArray(value);
257
+ }
258
+ function parseWindow(value) {
259
+ if (value === void 0 || value === null) return void 0;
260
+ if (!isRecord$2(value)) throw new Error("OpenAI Codex returned a malformed rate-limit window");
261
+ const usedPercent = value["used_percent"];
262
+ const windowSeconds = value["limit_window_seconds"];
263
+ if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) throw new Error("OpenAI Codex returned an invalid used percentage");
264
+ if (typeof windowSeconds !== "number" || !Number.isInteger(windowSeconds) || windowSeconds <= 0) throw new Error("OpenAI Codex returned an invalid rate-limit window duration");
265
+ return {
266
+ remainingPercent: 100 - usedPercent,
267
+ windowSeconds
268
+ };
269
+ }
270
+ function parseLimit(id, name, value) {
271
+ if (value === void 0 || value === null) return void 0;
272
+ if (!isRecord$2(value)) throw new Error("OpenAI Codex returned malformed rate-limit details");
273
+ const windows = [parseWindow(value["primary_window"]), parseWindow(value["secondary_window"])].filter((window) => window !== void 0);
274
+ return windows.length === 0 ? void 0 : {
275
+ id,
276
+ ...name === void 0 ? {} : { name },
277
+ windows
278
+ };
279
+ }
280
+ function exactAmount(record, key) {
281
+ const value = record[key];
282
+ if (typeof value !== "string" || value.length === 0 || value.length > 64 || !/^-?\d+(?:\.\d+)?$/u.test(value)) throw new Error(`OpenAI Codex returned an invalid ${key} amount`);
283
+ return value;
284
+ }
285
+ function parseCredits(value) {
286
+ if (value === void 0 || value === null) return void 0;
287
+ if (!isRecord$2(value) || typeof value["has_credits"] !== "boolean" || typeof value["unlimited"] !== "boolean") throw new Error("OpenAI Codex returned malformed credit details");
288
+ if (!value["has_credits"]) return void 0;
289
+ const balance = value["balance"];
290
+ if (balance !== void 0 && balance !== null && (typeof balance !== "string" || balance.length === 0 || balance.length > 64 || !/^-?\d+(?:\.\d+)?$/u.test(balance))) throw new Error("OpenAI Codex returned an invalid credit balance");
291
+ return {
292
+ unlimited: value["unlimited"],
293
+ ...typeof balance === "string" ? { balance } : {}
294
+ };
295
+ }
296
+ function parseIndividualLimit(value) {
297
+ if (value === void 0 || value === null) return void 0;
298
+ if (!isRecord$2(value)) throw new Error("OpenAI Codex returned malformed spend-control details");
299
+ const individual = value["individual_limit"];
300
+ if (individual === void 0 || individual === null) return void 0;
301
+ if (!isRecord$2(individual)) throw new Error("OpenAI Codex returned a malformed individual limit");
302
+ const remainingPercent = individual["remaining_percent"];
303
+ if (typeof remainingPercent !== "number" || !Number.isFinite(remainingPercent) || remainingPercent < 0 || remainingPercent > 100) throw new Error("OpenAI Codex returned an invalid individual-limit percentage");
304
+ return {
305
+ limit: exactAmount(individual, "limit"),
306
+ used: exactAmount(individual, "used"),
307
+ remaining: exactAmount(individual, "remaining"),
308
+ remainingPercent
309
+ };
310
+ }
311
+ /**
312
+ * Convert the provider response into the small secret-free object sent to the browser.
313
+ * @param value - opaque JSON returned by the ChatGPT usage endpoint.
314
+ * @returns core and additionally metered quota buckets with remaining percentages.
315
+ */
316
+ function parseOpenAICodexUsage(value) {
317
+ if (!isRecord$2(value)) throw new Error("OpenAI Codex returned a malformed usage response");
318
+ const limits = [];
319
+ const primary = parseLimit("codex", "Codex", value["rate_limit"]);
320
+ if (primary !== void 0) limits.push(primary);
321
+ const additional = value["additional_rate_limits"];
322
+ if (additional !== void 0 && additional !== null && !Array.isArray(additional)) throw new Error("OpenAI Codex returned malformed additional rate limits");
323
+ for (const item of additional ?? []) {
324
+ if (!isRecord$2(item)) throw new Error("OpenAI Codex returned a malformed additional rate limit");
325
+ const id = item["metered_feature"];
326
+ const name = item["limit_name"];
327
+ if (typeof id !== "string" || id.length === 0) throw new Error("OpenAI Codex returned an additional rate limit without an id");
328
+ if (name !== void 0 && name !== null && typeof name !== "string") throw new Error("OpenAI Codex returned an invalid additional rate-limit name");
329
+ const limit = parseLimit(id, typeof name === "string" && name.length > 0 ? name : void 0, item["rate_limit"]);
330
+ if (limit !== void 0) limits.push(limit);
331
+ }
332
+ const credits = parseCredits(value["credits"]);
333
+ const individualLimit = parseIndividualLimit(value["spend_control"]);
334
+ return {
335
+ rateLimits: limits,
336
+ ...credits === void 0 ? {} : { credits },
337
+ ...individualLimit === void 0 ? {} : { individualLimit }
338
+ };
339
+ }
340
+ /**
341
+ * Read current quota without issuing a model request. OAuth is refreshed through
342
+ * the same provider-native credential lifecycle used by normal Codex turns.
343
+ * @param store - plugin-owned OAuth credential store.
344
+ * @returns current rate-limit buckets safe to expose to the local browser page.
345
+ */
346
+ async function readOpenAICodexRateLimits(store) {
347
+ const models = createModels({ credentials: store });
348
+ models.setProvider(openaiCodexProvider());
349
+ const auth = await models.getAuth(OPENAI_CODEX_PROVIDER);
350
+ const credential = await store.read(OPENAI_CODEX_PROVIDER);
351
+ const access = auth?.auth.apiKey;
352
+ const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
353
+ if (access === void 0 || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("OpenAI Codex is signed out");
354
+ const response = await fetch(OPENAI_CODEX_USAGE_URL, {
355
+ method: "GET",
356
+ redirect: "error",
357
+ headers: {
358
+ authorization: `Bearer ${access}`,
359
+ "chatgpt-account-id": accountId,
360
+ accept: "application/json",
361
+ "cache-control": "no-store",
362
+ "user-agent": "dsh-codex-connect"
363
+ },
364
+ signal: AbortSignal.timeout(USAGE_REQUEST_TIMEOUT_MS)
365
+ });
366
+ if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "OpenAI Codex sign-in needs to be renewed" : `OpenAI Codex usage request failed with HTTP ${response.status}`);
367
+ let value;
368
+ try {
369
+ value = await response.json();
370
+ } catch (error) {
371
+ throw new Error("OpenAI Codex returned an unreadable usage response", { cause: error });
372
+ }
373
+ return parseOpenAICodexUsage(value);
374
+ }
375
+ //#endregion
376
+ //#region src/auth-paths.ts
377
+ /** Node-free route constants shared by the Host and browser plugin halves. */
378
+ /** Plugin-owned status endpoint consumed by its browser half. */
379
+ const OPENAI_CODEX_AUTH_STATUS_PATH = "/plugins/dsh-openai-codex/auth/status";
380
+ /** Plugin-owned browser-login endpoint consumed by its browser half. */
381
+ const OPENAI_CODEX_AUTH_LOGIN_PATH = "/plugins/dsh-openai-codex/auth/login";
382
+ /** Plugin-owned logout endpoint consumed by its browser half. */
383
+ const OPENAI_CODEX_AUTH_LOGOUT_PATH = "/plugins/dsh-openai-codex/auth/logout";
384
+ /** Redact provider diagnostics before they cross to the browser. */
385
+ function safeMessage(error) {
386
+ return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 1e3);
387
+ }
388
+ /** Reject with the prompt's abort reason while browser callback owns completion. */
389
+ function waitForPromptAbort(prompt) {
390
+ const signal = prompt.signal;
391
+ if (signal === void 0) return new Promise(() => {});
392
+ if (signal.aborted) return Promise.reject(signal.reason);
393
+ return new Promise((_resolve, reject) => {
394
+ signal.addEventListener("abort", () => {
395
+ reject(signal.reason);
396
+ }, { once: true });
397
+ });
398
+ }
399
+ /** One lifecycle owner for the callback server, challenge, and public status. */
400
+ var OpenAICodexWebAuth = class {
401
+ store;
402
+ state = { status: "signed-out" };
403
+ operation;
404
+ cancellation;
405
+ challenge;
406
+ challengeWaiters = [];
407
+ challengeTimer;
408
+ challengeTimeoutMs;
409
+ constructor(store, options = {}) {
410
+ this.store = store;
411
+ this.challengeTimeoutMs = options.challengeTimeoutMs ?? 3e4;
412
+ if (!Number.isFinite(this.challengeTimeoutMs) || this.challengeTimeoutMs <= 0) throw new TypeError("OpenAI Codex auth URL timeout must be a positive finite number");
413
+ }
414
+ /** Read current public state, consulting durable storage while idle. */
415
+ async status() {
416
+ if (this.operation !== void 0) return this.state;
417
+ if (this.state.status === "error") return this.state;
418
+ return this.readStoredStatus();
419
+ }
420
+ /** Start or join the current browser-login operation. */
421
+ async signIn() {
422
+ if (this.operation === void 0) this.start();
423
+ if (this.challenge !== void 0) return this.challenge;
424
+ return new Promise((resolve, reject) => {
425
+ this.challengeWaiters.push({
426
+ resolve,
427
+ reject
428
+ });
429
+ });
430
+ }
431
+ /** Cancel any callback listener, wait for quiescence, then delete the credential. */
432
+ async signOut() {
433
+ this.cancelSignIn(/* @__PURE__ */ new Error("OpenAI Codex sign-in cancelled"));
434
+ await this.operation?.catch(() => void 0);
435
+ await logoutOpenAICodex(this.store);
436
+ this.challenge = void 0;
437
+ this.state = { status: "signed-out" };
438
+ }
439
+ /** Stop the owned callback listener during plugin disposal. */
440
+ async dispose() {
441
+ this.cancelSignIn(/* @__PURE__ */ new Error("OpenAI Codex plugin disposed"));
442
+ await this.operation?.catch(() => void 0);
443
+ }
444
+ start() {
445
+ const cancellation = new AbortController();
446
+ this.cancellation = cancellation;
447
+ this.challenge = void 0;
448
+ this.state = { status: "signing-in" };
449
+ this.challengeTimer = setTimeout(() => {
450
+ this.cancelSignIn(/* @__PURE__ */ new Error(`OpenAI Codex did not provide an authorization URL within ${String(this.challengeTimeoutMs)}ms`));
451
+ }, this.challengeTimeoutMs);
452
+ this.challengeTimer.unref();
453
+ this.operation = loginOpenAICodex({
454
+ signal: cancellation.signal,
455
+ prompt: (prompt) => prompt.type === "select" ? Promise.resolve("browser") : waitForPromptAbort(prompt),
456
+ notify: (event) => {
457
+ this.onEvent(event);
458
+ }
459
+ }, this.store).then(async () => {
460
+ if (this.challenge === void 0) {
461
+ const error = /* @__PURE__ */ new Error("OpenAI Codex sign-in finished without an authorization URL");
462
+ this.rejectChallenge(error);
463
+ this.state = {
464
+ status: "error",
465
+ message: safeMessage(error)
466
+ };
467
+ return;
468
+ }
469
+ this.state = await this.readStoredStatus();
470
+ }, (error) => {
471
+ this.rejectChallenge(error);
472
+ this.state = {
473
+ status: "error",
474
+ message: safeMessage(error)
475
+ };
476
+ }).finally(() => {
477
+ this.clearChallengeTimer();
478
+ this.operation = void 0;
479
+ this.cancellation = void 0;
480
+ });
481
+ }
482
+ onEvent(event) {
483
+ if (event.type !== "auth_url") return;
484
+ let url;
485
+ try {
486
+ url = new URL(event.url);
487
+ } catch {
488
+ const error = /* @__PURE__ */ new Error("OpenAI returned an invalid authorization URL");
489
+ this.cancelSignIn(error);
490
+ return;
491
+ }
492
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") {
493
+ const error = /* @__PURE__ */ new Error("OpenAI returned an unsafe authorization URL");
494
+ this.cancelSignIn(error);
495
+ return;
496
+ }
497
+ const challenge = { url: event.url };
498
+ this.challenge = challenge;
499
+ this.clearChallengeTimer();
500
+ for (const waiter of this.challengeWaiters.splice(0)) waiter.resolve(challenge);
501
+ }
502
+ async readStoredStatus() {
503
+ if (!(await openAICodexAuthStatus(this.store)).authenticated) return { status: "signed-out" };
504
+ try {
505
+ return {
506
+ status: "signed-in",
507
+ usage: await readOpenAICodexRateLimits(this.store)
508
+ };
509
+ } catch (error) {
510
+ return {
511
+ status: "signed-in",
512
+ usage: { rateLimits: [] },
513
+ quotaError: safeMessage(error)
514
+ };
515
+ }
516
+ }
517
+ rejectChallenge(error) {
518
+ this.clearChallengeTimer();
519
+ for (const waiter of this.challengeWaiters.splice(0)) waiter.reject(error);
520
+ }
521
+ clearChallengeTimer() {
522
+ if (this.challengeTimer === void 0) return;
523
+ clearTimeout(this.challengeTimer);
524
+ this.challengeTimer = void 0;
525
+ }
526
+ cancelSignIn(error) {
527
+ this.rejectChallenge(error);
528
+ this.cancellation?.abort(error);
529
+ }
530
+ };
531
+ function loopbackHost(rawHost) {
532
+ if (/[\\/@?#]/u.test(rawHost)) return false;
533
+ try {
534
+ const parsed = new URL(`http://${rawHost}`);
535
+ if (parsed.username !== "" || parsed.password !== "" || parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") return false;
536
+ const hostname = (parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname).toLowerCase().replace(/\.$/u, "");
537
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "::ffff:127.0.0.1";
538
+ } catch {
539
+ return false;
540
+ }
541
+ }
542
+ function exactOrigin(req, rawHost, rawOrigin) {
543
+ try {
544
+ const origin = new URL(rawOrigin);
545
+ if (origin.username !== "" || origin.password !== "" || origin.pathname !== "/" || origin.search !== "" || origin.hash !== "") return false;
546
+ const encrypted = req.socket.encrypted === true;
547
+ return origin.origin === new URL(`${encrypted ? "https" : "http"}://${rawHost}`).origin;
548
+ } catch {
549
+ return false;
550
+ }
551
+ }
552
+ /** Whether a request comes from this loopback page rather than a remote/rebinding site. */
553
+ function trustedRequest(req) {
554
+ const remote = req.socket.remoteAddress;
555
+ if (remote !== "127.0.0.1" && remote !== "::1" && remote !== "::ffff:127.0.0.1") return false;
556
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
557
+ const host = req.headers.host;
558
+ if (typeof host !== "string" || !loopbackHost(host)) return false;
559
+ const origin = req.headers.origin;
560
+ if (origin === void 0) return true;
561
+ return typeof origin === "string" && exactOrigin(req, host, origin);
562
+ }
563
+ function json(res, status, value) {
564
+ res.writeHead(status, {
565
+ "content-type": "application/json; charset=utf-8",
566
+ "cache-control": "no-store",
567
+ "x-content-type-options": "nosniff"
568
+ });
569
+ res.end(JSON.stringify(value));
570
+ }
571
+ /** Register the plugin-owned OAuth routes when the Web server is composed. */
572
+ function registerOpenAICodexAuthRoutes(ctx, store) {
573
+ const auth = new OpenAICodexWebAuth(store);
574
+ ctx.effect(() => {
575
+ const routes = [
576
+ ctx.webServer.register({
577
+ kind: "exact",
578
+ path: OPENAI_CODEX_AUTH_STATUS_PATH,
579
+ handler: async (req, res) => {
580
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
581
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
582
+ json(res, 200, await auth.status());
583
+ }
584
+ }),
585
+ ctx.webServer.register({
586
+ kind: "exact",
587
+ path: OPENAI_CODEX_AUTH_LOGIN_PATH,
588
+ handler: async (req, res) => {
589
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
590
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
591
+ try {
592
+ json(res, 200, await auth.signIn());
593
+ } catch (error) {
594
+ json(res, 500, { error: safeMessage(error) });
595
+ }
596
+ }
597
+ }),
598
+ ctx.webServer.register({
599
+ kind: "exact",
600
+ path: OPENAI_CODEX_AUTH_LOGOUT_PATH,
601
+ handler: async (req, res) => {
602
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
603
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
604
+ try {
605
+ await auth.signOut();
606
+ json(res, 200, { ok: true });
607
+ } catch (error) {
608
+ json(res, 500, { error: safeMessage(error) });
609
+ }
610
+ }
611
+ })
612
+ ];
613
+ return async () => {
614
+ for (const dispose of routes) dispose();
615
+ await auth.dispose();
616
+ };
617
+ }, "dsh-codex-connect: Web OAuth routes");
618
+ }
619
+ //#endregion
620
+ //#region src/version.ts
621
+ const CODEX_CONNECT_VERSION = "0.1.0-alpha.4.3";
622
+ //#endregion
623
+ //#region src/doctor.ts
624
+ /** Secret-free diagnostics and duplicate-provider guidance. */
625
+ /** Actionable message for legacy/manual `openai-codex` adapter collisions. */
626
+ function openAICodexConflictMessage() {
627
+ return "Codex Connect cannot register provider \"openai-codex\" because another adapter already owns it. Remove or disable the legacy dsh-codex bundle or manual openai-codex provider row, then restart Harness.";
628
+ }
629
+ /** Fail before the generic registry error so the collision has a migration hint. */
630
+ function assertNoOpenAICodexProviderConflict(providerIds) {
631
+ if (providerIds.includes("openai-codex")) throw new Error(openAICodexConflictMessage());
632
+ }
633
+ /**
634
+ * Inspect only process and filesystem metadata. This function never opens the
635
+ * OAuth document, refreshes a token, or starts an authorization flow.
636
+ */
637
+ async function diagnoseOpenAICodex(options = {}) {
638
+ const path = options.credentialPath ?? openAICodexAuthPath();
639
+ let state = "missing";
640
+ let mode;
641
+ try {
642
+ const info = await lstat(path);
643
+ if (!info.isFile()) state = "not-a-regular-file";
644
+ else if (process.platform === "win32") state = "owner-only";
645
+ else {
646
+ mode = (info.mode & 511).toString(8).padStart(3, "0");
647
+ state = (info.mode & 63) === 0 ? "owner-only" : "permissions-too-broad";
648
+ }
649
+ } catch (error) {
650
+ state = error?.code === "ENOENT" ? "missing" : "unreadable-metadata";
651
+ }
652
+ const providerConflict = options.providerIds?.includes("openai-codex") ?? false;
653
+ const hints = [];
654
+ if (state === "missing") hints.push("Sign in only when you are ready; installation does not start OAuth.");
655
+ if (state === "permissions-too-broad") hints.push(`Restrict the OAuth file to its owner before use (current mode ${mode}).`);
656
+ if (state === "not-a-regular-file") hints.push("Replace the OAuth path with an owner-only regular file created by Codex Connect login.");
657
+ if (state === "unreadable-metadata") hints.push("Harness could not inspect the OAuth file metadata; check the parent directory and file ownership.");
658
+ if (providerConflict) hints.push(openAICodexConflictMessage());
659
+ if (!providerConflict) hints.push("If Harness reports a duplicate openai-codex adapter, remove the legacy bundle or manual provider row.");
660
+ return {
661
+ package: "dsh-codex-connect",
662
+ version: CODEX_CONNECT_VERSION,
663
+ node: process.version,
664
+ credentialFile: {
665
+ path,
666
+ state,
667
+ ...mode === void 0 ? {} : { mode }
668
+ },
669
+ capabilities: {
670
+ modelProvider: true,
671
+ search: options.enableSearch === true,
672
+ imageTool: options.enableImageTool === true,
673
+ changesHarnessDefaultModel: false,
674
+ changesHarnessSearchRoute: false
675
+ },
676
+ providerConflict,
677
+ hints
678
+ };
679
+ }
680
+ //#endregion
681
+ //#region src/public-http.ts
682
+ /** Public-network-only HTTP(S) reader used by the optional remote image path. */
683
+ /** Maximum time one DNS-plus-HTTP hop may occupy. */
684
+ const PUBLIC_HTTP_HOP_TIMEOUT_MS = 3e4;
685
+ function blockedList(family, ranges) {
686
+ const list = new BlockList();
687
+ for (const [address, prefix] of ranges) list.addSubnet(address, prefix, family);
688
+ return list;
689
+ }
690
+ const BLOCKED_IPV4 = blockedList("ipv4", [
691
+ ["0.0.0.0", 8],
692
+ ["10.0.0.0", 8],
693
+ ["100.64.0.0", 10],
694
+ ["127.0.0.0", 8],
695
+ ["169.254.0.0", 16],
696
+ ["172.16.0.0", 12],
697
+ ["192.0.0.0", 24],
698
+ ["192.0.2.0", 24],
699
+ ["192.88.99.0", 24],
700
+ ["192.168.0.0", 16],
701
+ ["198.18.0.0", 15],
702
+ ["198.51.100.0", 24],
703
+ ["203.0.113.0", 24],
704
+ ["224.0.0.0", 4],
705
+ ["240.0.0.0", 4]
706
+ ]);
707
+ const GLOBAL_IPV6 = blockedList("ipv6", [["2000::", 3]]);
708
+ const BLOCKED_IPV6 = blockedList("ipv6", [
709
+ ["2001::", 32],
710
+ ["2001:2::", 48],
711
+ ["2001:10::", 28],
712
+ ["2001:20::", 28],
713
+ ["2001:db8::", 32],
714
+ ["2002::", 16]
715
+ ]);
716
+ function unbracket(hostname) {
717
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
718
+ }
719
+ /** Whether an address is ordinary public unicast rather than a local/special target. */
720
+ function isPublicNetworkAddress(rawAddress) {
721
+ const address = unbracket(rawAddress);
722
+ if (address.includes("%")) return false;
723
+ const family = isIP(address);
724
+ if (family === 4) return !BLOCKED_IPV4.check(address, "ipv4");
725
+ if (family === 6) return GLOBAL_IPV6.check(address, "ipv6") && !BLOCKED_IPV6.check(address, "ipv6");
726
+ return false;
727
+ }
728
+ function abortError(signal) {
729
+ return signal.reason instanceof Error ? signal.reason : new Error(signal.reason === void 0 ? "remote image request aborted" : String(signal.reason));
730
+ }
731
+ function assertTargetUrl(url) {
732
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("view_image URL must use http or https");
733
+ if (url.username !== "" || url.password !== "") throw new Error("view_image URL must not contain credentials");
734
+ }
735
+ function normalizeAddress(candidate) {
736
+ if (candidate.family !== 4 && candidate.family !== 6) throw new Error("remote image hostname resolved to an unsupported address family");
737
+ return {
738
+ address: candidate.address,
739
+ family: candidate.family
740
+ };
741
+ }
742
+ async function resolveHost(hostname, signal) {
743
+ if (signal.aborted) throw abortError(signal);
744
+ const literal = unbracket(hostname);
745
+ const family = isIP(literal);
746
+ if (family === 4 || family === 6) return [{
747
+ address: literal,
748
+ family
749
+ }];
750
+ const results = await lookup(literal, {
751
+ all: true,
752
+ order: "verbatim"
753
+ });
754
+ if (signal.aborted) throw abortError(signal);
755
+ return results.map(normalizeAddress);
756
+ }
757
+ /** Collect one response body while enforcing declared and streaming size limits. */
758
+ async function collectBoundedBytes(body, declaredLength, maxBytes, signal) {
759
+ const declared = declaredLength === void 0 ? NaN : Number(declaredLength);
760
+ if (Number.isFinite(declared) && declared > maxBytes) throw new Error(`remote image exceeds ${String(maxBytes)} bytes`);
761
+ const chunks = [];
762
+ let total = 0;
763
+ for await (const chunk of body) {
764
+ if (signal.aborted) throw abortError(signal);
765
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : new Uint8Array(chunk);
766
+ total += bytes.byteLength;
767
+ if (total > maxBytes) throw new Error(`remote image exceeds ${String(maxBytes)} bytes`);
768
+ chunks.push(bytes);
769
+ }
770
+ const data = new Uint8Array(total);
771
+ let offset = 0;
772
+ for (const chunk of chunks) {
773
+ data.set(chunk, offset);
774
+ offset += chunk.byteLength;
775
+ }
776
+ return data;
777
+ }
778
+ function pinnedLookup(address) {
779
+ return (_hostname, options, callback) => {
780
+ const resolved = {
781
+ address: address.address,
782
+ family: address.family
783
+ };
784
+ if (options.all === true) callback(null, [resolved]);
785
+ else callback(null, resolved.address, resolved.family);
786
+ };
787
+ }
788
+ function headerValue(message, name) {
789
+ const value = message.headers[name];
790
+ return Array.isArray(value) ? value[0] : value;
791
+ }
792
+ async function requestPinned(url, address, maxBytes, signal) {
793
+ if (signal.aborted) throw abortError(signal);
794
+ return new Promise((resolve, reject) => {
795
+ let settled = false;
796
+ let response;
797
+ const finish = (result) => {
798
+ if (settled) return;
799
+ settled = true;
800
+ clearTimeout(timer);
801
+ signal.removeEventListener("abort", onAbort);
802
+ if (result.ok) resolve(result.value);
803
+ else reject(result.error);
804
+ };
805
+ const request$2 = (url.protocol === "https:" ? request$1 : request)(url, {
806
+ method: "GET",
807
+ agent: false,
808
+ lookup: pinnedLookup(address),
809
+ headers: { accept: "image/png, image/jpeg, image/webp, image/gif" }
810
+ }, (incoming) => {
811
+ response = incoming;
812
+ const status = incoming.statusCode ?? 0;
813
+ const location = headerValue(incoming, "location");
814
+ if (status >= 300 && status < 400 || status < 200 || status >= 300) {
815
+ finish({
816
+ ok: true,
817
+ value: {
818
+ status,
819
+ ...location === void 0 ? {} : { location }
820
+ }
821
+ });
822
+ incoming.destroy();
823
+ return;
824
+ }
825
+ collectBoundedBytes(incoming, headerValue(incoming, "content-length"), maxBytes, signal).then((data) => {
826
+ finish({
827
+ ok: true,
828
+ value: {
829
+ status,
830
+ data
831
+ }
832
+ });
833
+ }, (error) => {
834
+ incoming.destroy(error instanceof Error ? error : void 0);
835
+ finish({
836
+ ok: false,
837
+ error
838
+ });
839
+ });
840
+ });
841
+ const onAbort = () => {
842
+ const error = abortError(signal);
843
+ response?.destroy(error);
844
+ request$2.destroy(error);
845
+ };
846
+ const timer = setTimeout(() => {
847
+ const error = /* @__PURE__ */ new Error(`remote image request exceeded ${String(PUBLIC_HTTP_HOP_TIMEOUT_MS)}ms`);
848
+ response?.destroy(error);
849
+ request$2.destroy(error);
850
+ }, PUBLIC_HTTP_HOP_TIMEOUT_MS);
851
+ timer.unref();
852
+ signal.addEventListener("abort", onAbort, { once: true });
853
+ request$2.once("error", (error) => {
854
+ finish({
855
+ ok: false,
856
+ error
857
+ });
858
+ });
859
+ request$2.end();
860
+ });
861
+ }
862
+ /** Production resolver and one-shot agent which pins the validated address. */
863
+ const NODE_PUBLIC_HTTP_RUNTIME = {
864
+ resolve: resolveHost,
865
+ get: requestPinned
866
+ };
867
+ /** Fetch bytes from a public HTTP(S) target, revalidating and repinning each redirect. */
868
+ async function fetchPublicHttpResource(source, maxBytes, signal, runtime = NODE_PUBLIC_HTTP_RUNTIME) {
869
+ let url = new URL(source);
870
+ assertTargetUrl(url);
871
+ for (let redirects = 0;; redirects += 1) {
872
+ if (signal.aborted) throw abortError(signal);
873
+ const addresses = await runtime.resolve(url.hostname, signal);
874
+ if (addresses.length === 0 || addresses.some((candidate) => !isPublicNetworkAddress(candidate.address))) throw new Error(`remote image host ${JSON.stringify(url.hostname)} must resolve only to public network addresses`);
875
+ const hop = await runtime.get(url, addresses[0], maxBytes, signal);
876
+ if (hop.status >= 300 && hop.status < 400) {
877
+ if (redirects >= 5) throw new Error(`remote image exceeded ${String(5)} redirects`);
878
+ if (hop.location === void 0) throw new Error(`remote image redirect ${String(hop.status)} has no location`);
879
+ url = new URL(hop.location, url);
880
+ assertTargetUrl(url);
881
+ continue;
882
+ }
883
+ if (hop.status < 200 || hop.status >= 300) throw new Error(`remote image request failed with HTTP ${String(hop.status)}`);
884
+ if (hop.data === void 0) throw new Error("remote image response did not contain a body");
885
+ const name = basename(url.pathname) || void 0;
886
+ return {
887
+ data: hop.data,
888
+ display: url.href,
889
+ ...name === void 0 ? {} : { name }
890
+ };
891
+ }
892
+ }
893
+ //#endregion
894
+ //#region src/view-image.ts
895
+ /** Codex-compatible `view_image` tool for local paths and HTTP(S) URLs. */
896
+ /** Stable Codex tool name. */
897
+ const VIEW_IMAGE_TOOL_NAME = "view_image";
898
+ function refOf(image) {
899
+ return {
900
+ attachmentId: AttachmentId(image.attachmentId),
901
+ mediaType: image.mediaType,
902
+ bytes: image.bytes,
903
+ width: image.width,
904
+ height: image.height,
905
+ ...image.name === void 0 ? {} : { name: image.name }
906
+ };
907
+ }
908
+ function contentOf(value) {
909
+ return [{
910
+ type: "text",
911
+ text: `<source>${value.source}</source>\n<image>${value.image.mediaType}, ${value.image.width}x${value.image.height} px, ${value.image.bytes} bytes</image>`
912
+ }, {
913
+ type: "image",
914
+ attachment: refOf(value.image)
915
+ }];
916
+ }
917
+ function mediaTypeOf(data) {
918
+ if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
919
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
920
+ if (data.length >= 6) {
921
+ const signature = String.fromCharCode(...data.subarray(0, 6));
922
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
923
+ }
924
+ if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
925
+ }
926
+ async function assertImageCapable(ctx, exec, source) {
927
+ const configured = exec.agent?.session.requestHeader()?.config;
928
+ const provider = configured?.provider ?? exec.agent?.options.provider;
929
+ const model = configured?.model ?? exec.agent?.options.model;
930
+ if (provider === void 0 || model === void 0) throw new Error(`cannot view ${JSON.stringify(source)}: the current model route is unavailable`);
931
+ const info = await ctx.llm.resolveModelInfo(provider, model, exec.signal);
932
+ if (info.inputModalities === void 0 || !info.inputModalities.includes("image")) throw new Error(`cannot view ${JSON.stringify(source)}: model "${model}" does not declare image input`);
933
+ }
934
+ /** Build the plugin-owned image viewing tool. */
935
+ function viewImageTool(ctx) {
936
+ return defineTool({
937
+ name: VIEW_IMAGE_TOOL_NAME,
938
+ description: "View an image from a local file path or an http(s) URL. Returns the actual PNG, JPEG, WebP, or GIF image to vision-capable models.",
939
+ parameters: { source: {
940
+ type: "string",
941
+ required: true,
942
+ description: "Local absolute/relative image path, or an http(s) image URL."
943
+ } },
944
+ output: {
945
+ schema: {
946
+ type: "object",
947
+ additionalProperties: false,
948
+ properties: {
949
+ source: {
950
+ type: "string",
951
+ required: true
952
+ },
953
+ image: {
954
+ type: "object",
955
+ required: true,
956
+ additionalProperties: false,
957
+ properties: {
958
+ attachmentId: {
959
+ type: "string",
960
+ required: true
961
+ },
962
+ mediaType: {
963
+ type: "string",
964
+ required: true,
965
+ enum: [
966
+ "image/png",
967
+ "image/jpeg",
968
+ "image/webp",
969
+ "image/gif"
970
+ ]
971
+ },
972
+ bytes: {
973
+ type: "integer",
974
+ required: true
975
+ },
976
+ width: {
977
+ type: "integer",
978
+ required: true
979
+ },
980
+ height: {
981
+ type: "integer",
982
+ required: true
983
+ },
984
+ name: { type: "string" }
985
+ }
986
+ }
987
+ }
988
+ },
989
+ render: (_args, value) => contentOf(value)
990
+ },
991
+ isConcurrencySafe: () => true,
992
+ async execute(args, exec) {
993
+ const source = args.source.trim();
994
+ if (source.length === 0) throw new Error("view_image source must not be empty");
995
+ await assertImageCapable(ctx, exec, source);
996
+ const attachments = ctx.attachments;
997
+ const maxBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
998
+ let loaded;
999
+ if (/^https?:\/\//iu.test(source)) loaded = await fetchPublicHttpResource(source, maxBytes, exec.signal);
1000
+ else {
1001
+ const cwd = exec.agent?.session.header.cwd;
1002
+ const target = await ctx.fs.resolve(source, {
1003
+ ...cwd === void 0 ? {} : { cwd },
1004
+ signal: exec.signal
1005
+ });
1006
+ const info = await ctx.fs.stat(target, exec.signal);
1007
+ if (info === void 0) throw new Error(`image path does not exist: ${source}`);
1008
+ if (info.type !== "file") throw new Error(`image path is not a regular file: ${source}`);
1009
+ loaded = {
1010
+ data: await ctx.fs.readBytes(target, exec.signal, maxBytes),
1011
+ display: target.displayPath,
1012
+ name: basename(target.displayPath)
1013
+ };
1014
+ ctx.emit("fs/observed", target, {
1015
+ kind: "present",
1016
+ version: info.version
1017
+ }, exec);
1018
+ }
1019
+ const mediaType = mediaTypeOf(loaded.data);
1020
+ if (mediaType === void 0) throw new Error("view_image supports PNG, JPEG, WebP, and GIF image bytes");
1021
+ if (!attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`${mediaType} images are disabled by this deployment`);
1022
+ const ref = await attachments.saveImage({
1023
+ data: loaded.data,
1024
+ mediaType,
1025
+ ...loaded.name === void 0 ? {} : { name: loaded.name }
1026
+ });
1027
+ const value = {
1028
+ source: loaded.display,
1029
+ image: {
1030
+ attachmentId: ref.attachmentId,
1031
+ mediaType: ref.mediaType,
1032
+ bytes: ref.bytes,
1033
+ width: ref.width,
1034
+ height: ref.height,
1035
+ ...ref.name === void 0 ? {} : { name: ref.name }
1036
+ }
1037
+ };
1038
+ if (exec.parent !== void 0) exec.deferContext(createUserMessage({
1039
+ content: contentOf(value),
1040
+ source: {
1041
+ kind: "plugin",
1042
+ plugin: "dsh-codex-connect"
1043
+ }
1044
+ }));
1045
+ return value;
1046
+ },
1047
+ presentCall: (args) => ({
1048
+ card: "generic",
1049
+ title: `View image ${args.source}`,
1050
+ kind: /^https?:\/\//iu.test(args.source) ? "fetch" : "read",
1051
+ .../^https?:\/\//iu.test(args.source) ? { rawInput: args.source } : { locations: [{ path: args.source }] }
1052
+ })
1053
+ });
1054
+ }
1055
+ //#endregion
1056
+ //#region src/search-event.ts
1057
+ /** Dedicated log event written before an OpenAI Codex search dispatch. */
1058
+ const OPENAI_CODEX_SEARCH_MODEL_REQUEST_EVENT = "web/openai-codex-search-llm-request";
1059
+ /**
1060
+ * Register the plugin-owned event in the running Harness vocabulary. The
1061
+ * public DSH build exports its known-event collection as read-only because
1062
+ * core code must not mutate it accidentally; the runtime value is the Set
1063
+ * deliberately consulted on every persistence read. Registration remains for
1064
+ * the process lifetime so sessions written before an HMR cycle stay readable.
1065
+ */
1066
+ function installOpenAICodexSearchEvent() {
1067
+ if (!(KNOWN_SESSION_EVENT_TYPES instanceof Set)) throw new Error("dsh-codex-connect: this Harness build does not expose an extensible session event vocabulary");
1068
+ KNOWN_SESSION_EVENT_TYPES.add(OPENAI_CODEX_SEARCH_MODEL_REQUEST_EVENT);
1069
+ }
1070
+ /**
1071
+ * Append one resolved request to the initiating agent's session. Searches
1072
+ * outside an agent turn have no owning session and therefore produce no log.
1073
+ * @param ctx - plugin context carrying the optional active-agent service.
1074
+ * @param request - exact request after defaults, excluding credentials.
1075
+ */
1076
+ function recordOpenAICodexSearchRequest(ctx, request) {
1077
+ ctx.get("agents")?.currentInitiator()?.session.append(OPENAI_CODEX_SEARCH_MODEL_REQUEST_EVENT, request);
1078
+ }
1079
+ //#endregion
1080
+ //#region src/settings-contract.ts
1081
+ /** Node-free settings contract shared by the Host plugin and browser card. */
1082
+ /** Stable Harness settings namespace owned by this plugin. */
1083
+ const OPENAI_CODEX_SETTINGS_NAMESPACE = "llm-openai-codex";
1084
+ /** Default model used by the standalone search endpoint. */
1085
+ const DEFAULT_OPENAI_CODEX_SEARCH_MODEL = "gpt-5.6-sol";
1086
+ /** Default search mode, matching the official local Codex client. */
1087
+ const DEFAULT_OPENAI_CODEX_SEARCH_MODE = "cached";
1088
+ /** Default provider search-context size. */
1089
+ const DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE = "medium";
1090
+ /** Default output budget for the standalone search response. */
1091
+ const DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS = 1e4;
1092
+ const DEFAULT_OPENAI_CODEX_SETTINGS = Object.freeze({
1093
+ enableSearch: false,
1094
+ enableImageTool: false,
1095
+ searchModel: DEFAULT_OPENAI_CODEX_SEARCH_MODEL,
1096
+ searchMode: DEFAULT_OPENAI_CODEX_SEARCH_MODE,
1097
+ searchContextSize: DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE,
1098
+ searchMaxOutputTokens: DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS
1099
+ });
1100
+ /** Fill the schema defaults even when called without Cordis validation. */
1101
+ function resolveOpenAICodexSettings(value) {
1102
+ return {
1103
+ ...DEFAULT_OPENAI_CODEX_SETTINGS,
1104
+ ...value
1105
+ };
1106
+ }
1107
+ function isRecord$1(value) {
1108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1109
+ }
1110
+ /** Narrow the redacted settings wire payload before it enters React state. */
1111
+ function decodeOpenAICodexSettings(value) {
1112
+ if (!isRecord$1(value)) return void 0;
1113
+ const enableSearch = value["enableSearch"];
1114
+ const enableImageTool = value["enableImageTool"];
1115
+ const searchModel = value["searchModel"];
1116
+ const searchMode = value["searchMode"];
1117
+ const searchContextSize = value["searchContextSize"];
1118
+ const searchMaxOutputTokens = value["searchMaxOutputTokens"];
1119
+ if (typeof enableSearch !== "boolean" || typeof enableImageTool !== "boolean") return void 0;
1120
+ if (typeof searchModel !== "string" || searchModel.trim().length === 0) return void 0;
1121
+ if (searchMode !== "cached" && searchMode !== "indexed" && searchMode !== "live") return void 0;
1122
+ if (searchContextSize !== "low" && searchContextSize !== "medium" && searchContextSize !== "high") return void 0;
1123
+ if (typeof searchMaxOutputTokens !== "number" || !Number.isInteger(searchMaxOutputTokens) || searchMaxOutputTokens < 1) return void 0;
1124
+ return {
1125
+ enableSearch,
1126
+ enableImageTool,
1127
+ searchModel,
1128
+ searchMode,
1129
+ searchContextSize,
1130
+ searchMaxOutputTokens
1131
+ };
1132
+ }
1133
+ //#endregion
1134
+ //#region src/search.ts
1135
+ /**
1136
+ * OpenAI Codex standalone web search over the dsh web provider seam.
1137
+ * @module dsh-codex-connect/search
1138
+ */
1139
+ /** Stable dsh web-provider id selected by the bundle patch. */
1140
+ const OPENAI_CODEX_SEARCH_PROVIDER = OPENAI_CODEX_PROVIDER;
1141
+ /** Trusted first-party Codex base; OAuth credentials never cross to a configured origin. */
1142
+ const OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
1143
+ /** Standalone search endpoint used by the official Codex client. */
1144
+ const OPENAI_CODEX_SEARCH_URL = `${OPENAI_CODEX_BASE_URL}/alpha/search`;
1145
+ /** Convert the configured mode to the official endpoint field. */
1146
+ function externalWebAccess(mode) {
1147
+ switch (mode) {
1148
+ case "cached": return false;
1149
+ case "indexed": return "indexed";
1150
+ case "live": return true;
1151
+ }
1152
+ }
1153
+ /** Extract the account id paired with one OAuth access token. */
1154
+ function accountIdFromToken(access) {
1155
+ try {
1156
+ const parts = access.split(".");
1157
+ if (parts.length !== 3 || parts[1] === void 0) throw new Error("invalid JWT");
1158
+ const auth = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"))["https://api.openai.com/auth"];
1159
+ if (typeof auth !== "object" || auth === null || Array.isArray(auth)) throw new Error("missing auth claim");
1160
+ const accountId = auth["chatgpt_account_id"];
1161
+ if (typeof accountId !== "string" || accountId.length === 0) throw new Error("missing account id");
1162
+ return accountId;
1163
+ } catch (error) {
1164
+ throw new WebError("OpenAI Codex search credential has no usable account id; run \"dsh openai-codex login\" again", "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
1165
+ }
1166
+ }
1167
+ /** Whether an opaque value is a non-array record. */
1168
+ function isRecord(value) {
1169
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1170
+ }
1171
+ /** Read an optional non-empty string field. */
1172
+ function optionalString(record, key) {
1173
+ const value = record[key];
1174
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1175
+ }
1176
+ /** Accept only citeable HTTP(S) URLs from opaque result DTOs. */
1177
+ function citeableUrl(value) {
1178
+ if (typeof value !== "string") return void 0;
1179
+ try {
1180
+ const url = new URL(value);
1181
+ return url.protocol === "http:" || url.protocol === "https:" ? value : void 0;
1182
+ } catch {
1183
+ return;
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Map the standalone endpoint's forward-compatible result DTOs into the dsh
1188
+ * web result. Unknown DTO types and fields are ignored; malformed envelope
1189
+ * fields fail at the network boundary.
1190
+ * @param value - parsed response JSON.
1191
+ * @returns normalized answer and citeable sources.
1192
+ */
1193
+ function mapOpenAICodexSearchResponse(value) {
1194
+ if (!isRecord(value) || typeof value["output"] !== "string") throw new WebError("OpenAI Codex returned a search response without string output", "WEB_PROVIDER_ERROR");
1195
+ const output = value["output"];
1196
+ const rawResults = value["results"];
1197
+ if (rawResults !== void 0 && !Array.isArray(rawResults)) throw new WebError("OpenAI Codex returned a search response with non-array results", "WEB_PROVIDER_ERROR");
1198
+ const sources = [];
1199
+ const seen = /* @__PURE__ */ new Set();
1200
+ for (const item of rawResults ?? []) {
1201
+ if (!isRecord(item) || item["type"] !== "text_result") continue;
1202
+ const url = citeableUrl(item["url"]);
1203
+ if (url === void 0 || seen.has(url)) continue;
1204
+ seen.add(url);
1205
+ const title = optionalString(item, "title");
1206
+ const snippet = optionalString(item, "snippet");
1207
+ sources.push({
1208
+ url,
1209
+ ...title === void 0 ? {} : { title },
1210
+ ...snippet === void 0 ? {} : { snippet }
1211
+ });
1212
+ }
1213
+ return {
1214
+ ...output.length === 0 ? {} : { content: output },
1215
+ sources,
1216
+ truncated: false
1217
+ };
1218
+ }
1219
+ /** Stable cancellation error for every provider phase. */
1220
+ function searchAborted(signal, fallback) {
1221
+ return new WebError("OpenAI Codex search aborted", "WEB_ABORTED", { cause: signal?.aborted === true ? signal.reason : fallback });
1222
+ }
1223
+ /** Throw the provider's stable cancellation error when the caller already aborted. */
1224
+ function throwIfSearchAborted(signal) {
1225
+ if (signal?.aborted === true) throw searchAborted(signal);
1226
+ }
1227
+ /** True for native fetch cancellation. */
1228
+ function isAbortError(error) {
1229
+ return error instanceof DOMException && error.name === "AbortError";
1230
+ }
1231
+ /** Race an asynchronous auth refresh against caller cancellation. */
1232
+ function abortable(operation, signal) {
1233
+ if (signal === void 0) return operation;
1234
+ if (signal.aborted) return Promise.reject(searchAborted(signal));
1235
+ return new Promise((resolve, reject) => {
1236
+ const onAbort = () => {
1237
+ reject(searchAborted(signal));
1238
+ };
1239
+ signal.addEventListener("abort", onAbort, { once: true });
1240
+ operation.then((value) => {
1241
+ signal.removeEventListener("abort", onAbort);
1242
+ resolve(value);
1243
+ }, (error) => {
1244
+ signal.removeEventListener("abort", onAbort);
1245
+ reject(error);
1246
+ });
1247
+ });
1248
+ }
1249
+ /** Keep provider diagnostics bounded and remove JWT-like material. */
1250
+ function providerMessage(value) {
1251
+ if (!isRecord(value)) return void 0;
1252
+ const error = value["error"];
1253
+ return (typeof error === "string" ? error : isRecord(error) && typeof error["message"] === "string" ? error["message"] : typeof value["message"] === "string" ? value["message"] : void 0)?.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[REDACTED]").slice(0, 1e3);
1254
+ }
1255
+ /** OpenAI Codex standalone-search provider using the same refreshable OAuth store as the LLM route. */
1256
+ var OpenAICodexSearchProvider = class {
1257
+ options;
1258
+ id = OPENAI_CODEX_SEARCH_PROVIDER;
1259
+ models;
1260
+ /**
1261
+ * @param options - fixed trusted endpoint policy and deployment tunables.
1262
+ */
1263
+ constructor(options) {
1264
+ this.options = options;
1265
+ const models = createModels({ credentials: options.credentials });
1266
+ models.setProvider(openaiCodexProvider());
1267
+ this.models = models;
1268
+ }
1269
+ /** The local configuration is usable; credential presence is resolved per request. */
1270
+ available() {
1271
+ return this.options.model.length > 0 && Number.isInteger(this.options.maxOutputTokens) && this.options.maxOutputTokens > 0;
1272
+ }
1273
+ /** @inheritdoc */
1274
+ async search(request, signal) {
1275
+ throwIfSearchAborted(signal);
1276
+ let auth;
1277
+ try {
1278
+ auth = await abortable(this.models.getAuth(OPENAI_CODEX_PROVIDER), signal);
1279
+ } catch (error) {
1280
+ throwIfSearchAborted(signal);
1281
+ if (isAbortError(error)) throw searchAborted(signal, error);
1282
+ throw new WebError("OpenAI Codex search credential resolution failed", "WEB_PROVIDER_ERROR", { cause: error });
1283
+ }
1284
+ const access = auth?.auth.apiKey;
1285
+ if (access === void 0 || access.length === 0) throw new WebError("OpenAI Codex search is signed out; run \"dsh openai-codex login\"", "WEB_PROVIDER_CREDENTIAL_MISSING");
1286
+ const accountId = accountIdFromToken(access);
1287
+ throwIfSearchAborted(signal);
1288
+ const body = {
1289
+ id: this.options.resolveRequestId(),
1290
+ model: this.options.model,
1291
+ input: [{
1292
+ type: "message",
1293
+ role: "user",
1294
+ content: [{
1295
+ type: "input_text",
1296
+ text: request.query
1297
+ }]
1298
+ }],
1299
+ commands: { search_query: [{ q: request.query }] },
1300
+ settings: {
1301
+ search_context_size: this.options.contextSize,
1302
+ allowed_callers: ["direct"],
1303
+ external_web_access: externalWebAccess(this.options.mode)
1304
+ },
1305
+ max_output_tokens: this.options.maxOutputTokens
1306
+ };
1307
+ this.options.recordRequest?.({
1308
+ endpoint: OPENAI_CODEX_SEARCH_URL,
1309
+ body
1310
+ });
1311
+ throwIfSearchAborted(signal);
1312
+ let response;
1313
+ try {
1314
+ response = await fetch(OPENAI_CODEX_SEARCH_URL, {
1315
+ method: "POST",
1316
+ redirect: "error",
1317
+ headers: {
1318
+ authorization: `Bearer ${access}`,
1319
+ "chatgpt-account-id": accountId,
1320
+ "content-type": "application/json",
1321
+ accept: "application/json",
1322
+ originator: "deepseek-harness"
1323
+ },
1324
+ body: JSON.stringify(body),
1325
+ ...signal === void 0 ? {} : { signal }
1326
+ });
1327
+ } catch (error) {
1328
+ throwIfSearchAborted(signal);
1329
+ if (isAbortError(error)) throw searchAborted(signal, error);
1330
+ throw new WebError("OpenAI Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
1331
+ }
1332
+ let payload;
1333
+ try {
1334
+ payload = await response.json();
1335
+ } catch (error) {
1336
+ throwIfSearchAborted(signal);
1337
+ if (isAbortError(error)) throw searchAborted(signal, error);
1338
+ throw new WebError(`OpenAI Codex returned an unprocessable search response (HTTP ${response.status})`, "WEB_PROVIDER_ERROR", { cause: error });
1339
+ }
1340
+ if (!response.ok) {
1341
+ const detail = providerMessage(payload);
1342
+ const message = detail === void 0 ? `OpenAI Codex search failed (HTTP ${response.status})` : `OpenAI Codex search failed (HTTP ${response.status}): ${detail}`;
1343
+ throw new WebError(response.status === 401 || response.status === 403 ? `${message}; run "dsh openai-codex login" again` : message, response.status === 401 || response.status === 403 ? "WEB_PROVIDER_CREDENTIAL_MISSING" : "WEB_PROVIDER_ERROR");
1344
+ }
1345
+ return mapOpenAICodexSearchResponse(payload);
1346
+ }
1347
+ };
1348
+ //#endregion
1349
+ //#region src/index.ts
1350
+ /** Stable Cordis plugin name. */
1351
+ const name = "llm-openai-codex";
1352
+ /** The model registry required before the provider can register. */
1353
+ const inject = ["llm"];
1354
+ /** Branded Host settings namespace used by the configurable-provider directory. */
1355
+ const OPENAI_CODEX_SETTINGS_NS = settingsNamespace(OPENAI_CODEX_SETTINGS_NAMESPACE);
1356
+ const Config = z.object({
1357
+ enableSearch: z.boolean().default(false),
1358
+ enableImageTool: z.boolean().default(false),
1359
+ searchModel: z.string().default(DEFAULT_OPENAI_CODEX_SEARCH_MODEL),
1360
+ searchMode: z.union([
1361
+ "cached",
1362
+ "indexed",
1363
+ "live"
1364
+ ]).default(DEFAULT_OPENAI_CODEX_SEARCH_MODE),
1365
+ searchContextSize: z.union([
1366
+ "low",
1367
+ "medium",
1368
+ "high"
1369
+ ]).default(DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE),
1370
+ searchMaxOutputTokens: z.number().step(1).min(1).default(DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS)
1371
+ });
1372
+ /**
1373
+ * Register the `openai-codex` LLM route with one provider-native OAuth store.
1374
+ * Search and image tooling are added only when their config flags are true.
1375
+ * Selecting this route as the Harness default remains a separate profile choice.
1376
+ * @param ctx - plugin context carrying the LLM registry plus optional services.
1377
+ * @param config - capability gates and standalone-search tuning.
1378
+ */
1379
+ function apply(ctx, config) {
1380
+ let current = () => config;
1381
+ const credentials = new OpenAICodexCredentialStore();
1382
+ assertNoOpenAICodexProviderConflict(ctx.llm.listProviders().map((provider) => provider.id));
1383
+ ctx.llm.registerAdapter([OPENAI_CODEX_PROVIDER], createOpenAICodexAdapter(credentials, () => ctx.get("attachments")));
1384
+ ctx.llm.registerConfigurableProviders([{
1385
+ provider: OPENAI_CODEX_PROVIDER,
1386
+ displayName: "OpenAI Codex",
1387
+ settingsNs: OPENAI_CODEX_SETTINGS_NS,
1388
+ settingsPath: [],
1389
+ declared: false
1390
+ }]);
1391
+ ctx.inject(["webServer"], (webCtx) => registerOpenAICodexAuthRoutes(webCtx, credentials));
1392
+ let stopped = false;
1393
+ let searchFiber;
1394
+ let searchRegistration;
1395
+ let searchTail = Promise.resolve();
1396
+ let imageFiber;
1397
+ let imageTail = Promise.resolve();
1398
+ const reconcileSearch = async () => {
1399
+ if (stopped) return;
1400
+ const resolved = resolveOpenAICodexSettings(current());
1401
+ const nextRegistration = resolved.enableSearch ? {
1402
+ model: resolved.searchModel,
1403
+ mode: resolved.searchMode,
1404
+ contextSize: resolved.searchContextSize,
1405
+ maxOutputTokens: resolved.searchMaxOutputTokens
1406
+ } : void 0;
1407
+ if (deepEqualJson(nextRegistration, searchRegistration)) return;
1408
+ const previous = searchFiber;
1409
+ searchFiber = void 0;
1410
+ searchRegistration = void 0;
1411
+ if (previous !== void 0) await previous.dispose();
1412
+ if (stopped || nextRegistration === void 0) return;
1413
+ installOpenAICodexSearchEvent();
1414
+ const fiber = ctx.inject(["web"], (webCtx) => webCtx.web.registerSearchProvider(new OpenAICodexSearchProvider({
1415
+ credentials,
1416
+ model: nextRegistration.model,
1417
+ mode: nextRegistration.mode,
1418
+ contextSize: nextRegistration.contextSize,
1419
+ maxOutputTokens: nextRegistration.maxOutputTokens,
1420
+ resolveRequestId: () => String(webCtx.get("agents")?.currentInitiator()?.session.id ?? randomUUID()),
1421
+ recordRequest: (request) => {
1422
+ recordOpenAICodexSearchRequest(webCtx, request);
1423
+ }
1424
+ })));
1425
+ searchFiber = fiber;
1426
+ searchRegistration = nextRegistration;
1427
+ Promise.resolve(fiber).catch((error) => {
1428
+ if (searchFiber === fiber) {
1429
+ searchFiber = void 0;
1430
+ searchRegistration = void 0;
1431
+ }
1432
+ ctx.logger.error("dsh-codex-connect: optional search provider failed to activate");
1433
+ ctx.logger.error(error);
1434
+ });
1435
+ };
1436
+ const reconcileImageTool = async () => {
1437
+ if (stopped) return;
1438
+ const enabled = resolveOpenAICodexSettings(current()).enableImageTool;
1439
+ if (enabled === (imageFiber !== void 0)) return;
1440
+ const previous = imageFiber;
1441
+ imageFiber = void 0;
1442
+ if (previous !== void 0) await previous.dispose();
1443
+ if (stopped || !enabled) return;
1444
+ const fiber = ctx.inject([
1445
+ "tools",
1446
+ "fs",
1447
+ "attachments"
1448
+ ], (toolCtx) => toolCtx.tools.register(viewImageTool(toolCtx)));
1449
+ imageFiber = fiber;
1450
+ Promise.resolve(fiber).catch((error) => {
1451
+ if (imageFiber === fiber) imageFiber = void 0;
1452
+ ctx.logger.error("dsh-codex-connect: optional view_image tool failed to activate");
1453
+ ctx.logger.error(error);
1454
+ });
1455
+ };
1456
+ const scheduleCapabilities = () => {
1457
+ searchTail = searchTail.then(reconcileSearch, reconcileSearch).catch((error) => {
1458
+ ctx.logger.error("dsh-codex-connect: could not apply the updated search configuration");
1459
+ ctx.logger.error(error);
1460
+ });
1461
+ imageTail = imageTail.then(reconcileImageTool, reconcileImageTool).catch((error) => {
1462
+ ctx.logger.error("dsh-codex-connect: could not apply the updated image-tool configuration");
1463
+ ctx.logger.error(error);
1464
+ });
1465
+ };
1466
+ ctx.effect(() => async () => {
1467
+ stopped = true;
1468
+ await Promise.all([searchTail, imageTail]);
1469
+ const search = searchFiber;
1470
+ const image = imageFiber;
1471
+ searchFiber = void 0;
1472
+ imageFiber = void 0;
1473
+ await Promise.allSettled([search?.dispose() ?? Promise.resolve(), image?.dispose() ?? Promise.resolve()]);
1474
+ }, "dsh-codex-connect: optional capability lifecycle");
1475
+ installSettingsSection(ctx, OPENAI_CODEX_SETTINGS_NS, Config, config, {
1476
+ setSource(source) {
1477
+ current = source;
1478
+ },
1479
+ onChange: scheduleCapabilities
1480
+ });
1481
+ scheduleCapabilities();
1482
+ }
1483
+ //#endregion
1484
+ export { logoutOpenAICodex as A, assertNoOpenAICodexProviderConflict as C, parseOpenAICodexUsage as D, OPENAI_CODEX_USAGE_URL as E, openAICodexAuthPath as F, OPENAI_CODEX_AUTH_FILENAME as M, OPENAI_CODEX_PROVIDER as N, readOpenAICodexRateLimits as O, OpenAICodexCredentialStore as P, VIEW_IMAGE_TOOL_NAME as S, openAICodexConflictMessage as T, decodeOpenAICodexSettings as _, name as a, installOpenAICodexSearchEvent as b, OPENAI_CODEX_SEARCH_URL as c, DEFAULT_OPENAI_CODEX_SEARCH_CONTEXT_SIZE as d, DEFAULT_OPENAI_CODEX_SEARCH_MAX_OUTPUT_TOKENS as f, OPENAI_CODEX_SETTINGS_NAMESPACE as g, DEFAULT_OPENAI_CODEX_SETTINGS as h, inject as i, openAICodexAuthStatus as j, loginOpenAICodex as k, OpenAICodexSearchProvider as l, DEFAULT_OPENAI_CODEX_SEARCH_MODEL as m, OPENAI_CODEX_SETTINGS_NS as n, OPENAI_CODEX_BASE_URL as o, DEFAULT_OPENAI_CODEX_SEARCH_MODE as p, apply as r, OPENAI_CODEX_SEARCH_PROVIDER as s, Config as t, mapOpenAICodexSearchResponse as u, resolveOpenAICodexSettings as v, diagnoseOpenAICodex as w, recordOpenAICodexSearchRequest as x, OPENAI_CODEX_SEARCH_MODEL_REQUEST_EVENT as y };