@iam-brain/opencode-codex-auth 0.1.15 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/lib/accounts-tools.d.ts.map +1 -1
  2. package/dist/lib/accounts-tools.js +8 -3
  3. package/dist/lib/accounts-tools.js.map +1 -1
  4. package/dist/lib/codex-native/browser.d.ts +12 -0
  5. package/dist/lib/codex-native/browser.d.ts.map +1 -0
  6. package/dist/lib/codex-native/browser.js +38 -0
  7. package/dist/lib/codex-native/browser.js.map +1 -0
  8. package/dist/lib/codex-native/catalog-auth.d.ts +6 -0
  9. package/dist/lib/codex-native/catalog-auth.d.ts.map +1 -0
  10. package/dist/lib/codex-native/catalog-auth.js +32 -0
  11. package/dist/lib/codex-native/catalog-auth.js.map +1 -0
  12. package/dist/lib/codex-native/client-identity.d.ts +13 -0
  13. package/dist/lib/codex-native/client-identity.d.ts.map +1 -0
  14. package/dist/lib/codex-native/client-identity.js +342 -0
  15. package/dist/lib/codex-native/client-identity.js.map +1 -0
  16. package/dist/lib/codex-native/collaboration.d.ts +19 -0
  17. package/dist/lib/codex-native/collaboration.d.ts.map +1 -0
  18. package/dist/lib/codex-native/collaboration.js +126 -0
  19. package/dist/lib/codex-native/collaboration.js.map +1 -0
  20. package/dist/lib/codex-native/oauth-server.d.ts +28 -0
  21. package/dist/lib/codex-native/oauth-server.d.ts.map +1 -0
  22. package/dist/lib/codex-native/oauth-server.js +267 -0
  23. package/dist/lib/codex-native/oauth-server.js.map +1 -0
  24. package/dist/lib/codex-native/originator.d.ts +4 -0
  25. package/dist/lib/codex-native/originator.d.ts.map +1 -0
  26. package/dist/lib/codex-native/originator.js +12 -0
  27. package/dist/lib/codex-native/originator.js.map +1 -0
  28. package/dist/lib/codex-native/request-transform.d.ts +50 -0
  29. package/dist/lib/codex-native/request-transform.d.ts.map +1 -0
  30. package/dist/lib/codex-native/request-transform.js +353 -0
  31. package/dist/lib/codex-native/request-transform.js.map +1 -0
  32. package/dist/lib/codex-native.d.ts +5 -13
  33. package/dist/lib/codex-native.d.ts.map +1 -1
  34. package/dist/lib/codex-native.js +193 -1048
  35. package/dist/lib/codex-native.js.map +1 -1
  36. package/dist/lib/codex-quota-fetch.d.ts +2 -0
  37. package/dist/lib/codex-quota-fetch.d.ts.map +1 -1
  38. package/dist/lib/codex-quota-fetch.js +17 -12
  39. package/dist/lib/codex-quota-fetch.js.map +1 -1
  40. package/dist/lib/codex-status-tool.js.map +1 -1
  41. package/dist/lib/codex-status-ui.d.ts.map +1 -1
  42. package/dist/lib/codex-status-ui.js +4 -20
  43. package/dist/lib/codex-status-ui.js.map +1 -1
  44. package/dist/lib/compat-sanitizer.d.ts.map +1 -1
  45. package/dist/lib/compat-sanitizer.js +2 -1
  46. package/dist/lib/compat-sanitizer.js.map +1 -1
  47. package/dist/lib/config.d.ts.map +1 -1
  48. package/dist/lib/config.js +28 -70
  49. package/dist/lib/config.js.map +1 -1
  50. package/dist/lib/fetch-orchestrator.d.ts +1 -1
  51. package/dist/lib/fetch-orchestrator.d.ts.map +1 -1
  52. package/dist/lib/fetch-orchestrator.js +7 -11
  53. package/dist/lib/fetch-orchestrator.js.map +1 -1
  54. package/dist/lib/identity.d.ts.map +1 -1
  55. package/dist/lib/identity.js.map +1 -1
  56. package/dist/lib/installer-cli.d.ts.map +1 -1
  57. package/dist/lib/installer-cli.js +1 -3
  58. package/dist/lib/installer-cli.js.map +1 -1
  59. package/dist/lib/logger.d.ts.map +1 -1
  60. package/dist/lib/logger.js +13 -5
  61. package/dist/lib/logger.js.map +1 -1
  62. package/dist/lib/model-catalog.d.ts +2 -0
  63. package/dist/lib/model-catalog.d.ts.map +1 -1
  64. package/dist/lib/model-catalog.js +117 -41
  65. package/dist/lib/model-catalog.js.map +1 -1
  66. package/dist/lib/orchestrator-agents.d.ts.map +1 -1
  67. package/dist/lib/orchestrator-agents.js.map +1 -1
  68. package/dist/lib/paths.d.ts.map +1 -1
  69. package/dist/lib/paths.js.map +1 -1
  70. package/dist/lib/persona-tool-cli.d.ts.map +1 -1
  71. package/dist/lib/persona-tool-cli.js.map +1 -1
  72. package/dist/lib/persona-tool.d.ts.map +1 -1
  73. package/dist/lib/persona-tool.js +10 -31
  74. package/dist/lib/persona-tool.js.map +1 -1
  75. package/dist/lib/personalities.d.ts.map +1 -1
  76. package/dist/lib/personalities.js.map +1 -1
  77. package/dist/lib/proactive-refresh.d.ts.map +1 -1
  78. package/dist/lib/proactive-refresh.js +3 -6
  79. package/dist/lib/proactive-refresh.js.map +1 -1
  80. package/dist/lib/refresh-queue.d.ts.map +1 -1
  81. package/dist/lib/refresh-queue.js +4 -2
  82. package/dist/lib/refresh-queue.js.map +1 -1
  83. package/dist/lib/request-snapshots.d.ts.map +1 -1
  84. package/dist/lib/request-snapshots.js.map +1 -1
  85. package/dist/lib/rotation.d.ts.map +1 -1
  86. package/dist/lib/rotation.js +2 -5
  87. package/dist/lib/rotation.js.map +1 -1
  88. package/dist/lib/session-affinity.d.ts.map +1 -1
  89. package/dist/lib/session-affinity.js.map +1 -1
  90. package/dist/lib/storage.d.ts.map +1 -1
  91. package/dist/lib/storage.js +2 -6
  92. package/dist/lib/storage.js.map +1 -1
  93. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  94. package/dist/lib/ui/auth-menu.js +1 -3
  95. package/dist/lib/ui/auth-menu.js.map +1 -1
  96. package/dist/lib/ui/tty/ansi.d.ts.map +1 -1
  97. package/dist/lib/ui/tty/ansi.js.map +1 -1
  98. package/dist/lib/ui/tty/confirm.d.ts.map +1 -1
  99. package/dist/lib/ui/tty/confirm.js.map +1 -1
  100. package/dist/lib/ui/tty/select.d.ts.map +1 -1
  101. package/dist/lib/ui/tty/select.js +1 -3
  102. package/dist/lib/ui/tty/select.js.map +1 -1
  103. package/package.json +9 -2
@@ -1,12 +1,6 @@
1
- import http from "node:http";
2
- import os from "node:os";
3
- import { execFile, execFileSync } from "node:child_process";
4
- import { appendFileSync, chmodSync, mkdirSync, readFileSync } from "node:fs";
5
- import path from "node:path";
6
- import { promisify } from "node:util";
7
1
  import { extractAccountIdFromClaims as extractAccountIdFromClaimsBase, extractEmailFromClaims, extractPlanFromClaims, parseJwtClaims } from "./claims";
8
2
  import { CodexStatus } from "./codex-status";
9
- import { saveSnapshots } from "./codex-status-storage";
3
+ import { loadSnapshots, saveSnapshots } from "./codex-status-storage";
10
4
  import { PluginFatalError, formatWaitTime, isPluginFatalError, toSyntheticErrorResponse } from "./fatal-errors";
11
5
  import { buildIdentityKey, ensureIdentityKey, normalizeEmail, normalizePlan, synchronizeIdentityKey } from "./identity";
12
6
  import { defaultSessionAffinityPath, defaultSnapshotsPath } from "./paths";
@@ -18,11 +12,18 @@ import { formatToastMessage } from "./toast";
18
12
  import { runAuthMenuOnce } from "./ui/auth-menu-runner";
19
13
  import { applyCodexCatalogToProviderModels, getCodexModelCatalog, getRuntimeDefaultsForModel, resolveInstructionsForModel } from "./model-catalog";
20
14
  import { CODEX_RS_COMPACT_PROMPT } from "./orchestrator-agents";
21
- import { sanitizeRequestPayloadForCompat } from "./compat-sanitizer";
22
15
  import { fetchQuotaSnapshotFromBackend } from "./codex-quota-fetch";
23
16
  import { createRequestSnapshots } from "./request-snapshots";
24
17
  import { CODEX_OAUTH_SUCCESS_HTML } from "./oauth-pages";
18
+ import { mergeInstructions, resolveCollaborationInstructions, resolveCollaborationModeKind, resolveCollaborationProfile, resolveHookAgentName, resolveSubagentHeaderValue } from "./codex-native/collaboration";
19
+ import { applyCatalogInstructionOverrideToRequest, applyCodexRuntimeDefaultsToParams, findCatalogModelForCandidates, getModelLookupCandidates, getModelThinkingSummariesOverride, getVariantLookupCandidates, resolvePersonalityForModel, sanitizeOutboundRequestIfNeeded } from "./codex-native/request-transform";
25
20
  import { createSessionExistsFn, loadSessionAffinity, pruneSessionAffinitySnapshot, readSessionAffinitySnapshot, saveSessionAffinity, writeSessionAffinitySnapshot } from "./session-affinity";
21
+ import { resolveCodexOriginator } from "./codex-native/originator";
22
+ import { tryOpenUrlInBrowser as openUrlInBrowser } from "./codex-native/browser";
23
+ import { selectCatalogAuthCandidate } from "./codex-native/catalog-auth";
24
+ import { buildCodexUserAgent, refreshCodexClientVersionFromGitHub, resolveCodexClientVersion, resolveRequestUserAgent } from "./codex-native/client-identity";
25
+ import { createOAuthServerController } from "./codex-native/oauth-server";
26
+ export { browserOpenInvocationFor } from "./codex-native/browser";
26
27
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
27
28
  const ISSUER = "https://auth.openai.com";
28
29
  const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
@@ -55,10 +56,10 @@ const OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS = (() => {
55
56
  const parsed = Number(raw);
56
57
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
57
58
  })();
58
- const OAUTH_DEBUG_LOG_DIR = path.join(os.homedir(), ".config", "opencode", "logs", "codex-plugin");
59
- const OAUTH_DEBUG_LOG_FILE = path.join(OAUTH_DEBUG_LOG_DIR, "oauth-lifecycle.log");
60
- const execFileAsync = promisify(execFile);
61
- const DEFAULT_PLUGIN_VERSION = "0.1.0";
59
+ const OPENAI_OUTBOUND_HOST_ALLOWLIST = new Set(["api.openai.com", "auth.openai.com", "chat.openai.com", "chatgpt.com"]);
60
+ const AUTH_MENU_QUOTA_SNAPSHOT_TTL_MS = 60_000;
61
+ const AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS = 30_000;
62
+ const AUTH_MENU_QUOTA_FETCH_TIMEOUT_MS = 5000;
62
63
  const INTERNAL_COLLABORATION_MODE_HEADER = "x-opencode-collaboration-mode-kind";
63
64
  const SESSION_AFFINITY_MISSING_GRACE_MS = 15 * 60 * 1000;
64
65
  const CODEX_PLAN_MODE_INSTRUCTIONS = [
@@ -93,38 +94,12 @@ const STATIC_FALLBACK_MODELS = [
93
94
  function sleep(ms) {
94
95
  return new Promise((resolve) => setTimeout(resolve, ms));
95
96
  }
96
- export function browserOpenInvocationFor(url, platform = process.platform) {
97
- if (platform === "darwin") {
98
- return { command: "open", args: [url] };
99
- }
100
- if (platform === "win32") {
101
- return { command: "cmd", args: ["/c", "start", "", url] };
102
- }
103
- return { command: "xdg-open", args: [url] };
104
- }
105
97
  export async function tryOpenUrlInBrowser(url, log) {
106
- if (process.env.OPENCODE_NO_BROWSER === "1")
107
- return false;
108
- if (process.env.VITEST || process.env.NODE_ENV === "test")
109
- return false;
110
- const invocation = browserOpenInvocationFor(url);
111
- emitOAuthDebug("browser_open_attempt", { command: invocation.command });
112
- try {
113
- await execFileAsync(invocation.command, invocation.args, { windowsHide: true, timeout: 5000 });
114
- emitOAuthDebug("browser_open_success", { command: invocation.command });
115
- return true;
116
- }
117
- catch (error) {
118
- emitOAuthDebug("browser_open_failure", {
119
- command: invocation.command,
120
- error: error instanceof Error ? error.message : String(error)
121
- });
122
- log?.warn("failed to auto-open oauth URL", {
123
- command: invocation.command,
124
- error: error instanceof Error ? error.message : String(error)
125
- });
126
- return false;
127
- }
98
+ return openUrlInBrowser({
99
+ url,
100
+ log,
101
+ onEvent: (event, meta) => oauthServerController.emitDebug(event, meta ?? {})
102
+ });
128
103
  }
129
104
  function escapeHtml(value) {
130
105
  return value
@@ -141,11 +116,7 @@ async function generatePKCE() {
141
116
  return { verifier, challenge };
142
117
  }
143
118
  function base64UrlEncode(bytes) {
144
- return Buffer.from(bytes)
145
- .toString("base64")
146
- .replace(/\+/g, "-")
147
- .replace(/\//g, "_")
148
- .replace(/=+$/g, "");
119
+ return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
149
120
  }
150
121
  function generateState() {
151
122
  return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
@@ -195,6 +166,8 @@ export const __testOnly = {
195
166
  modeForRuntimeMode,
196
167
  buildCodexUserAgent,
197
168
  resolveRequestUserAgent,
169
+ resolveCodexClientVersion,
170
+ refreshCodexClientVersionFromGitHub,
198
171
  resolveHookAgentName,
199
172
  resolveCollaborationModeKind,
200
173
  resolveSubagentHeaderValue,
@@ -278,8 +251,7 @@ function composeCodexSuccessRedirectUrl(tokens, options = {}) {
278
251
  const port = options.port ?? OAUTH_PORT;
279
252
  const idClaims = getOpenAIAuthClaims(tokens.id_token);
280
253
  const accessClaims = getOpenAIAuthClaims(tokens.access_token);
281
- const needsSetup = !getClaimBoolean(idClaims, "completed_platform_onboarding") &&
282
- getClaimBoolean(idClaims, "is_org_owner");
254
+ const needsSetup = !getClaimBoolean(idClaims, "completed_platform_onboarding") && getClaimBoolean(idClaims, "is_org_owner");
283
255
  const platformUrl = issuer === ISSUER ? "https://platform.openai.com" : "https://platform.api.openai.org";
284
256
  const params = new URLSearchParams({
285
257
  needs_setup: String(needsSetup),
@@ -375,254 +347,32 @@ function buildOAuthErrorHtml(error) {
375
347
  </body>
376
348
  </html>`;
377
349
  }
378
- let oauthServer;
379
- let pendingOAuth;
380
- let oauthServerCloseTimer;
350
+ const oauthServerController = createOAuthServerController({
351
+ port: OAUTH_PORT,
352
+ loopbackHost: OAUTH_LOOPBACK_HOST,
353
+ callbackOrigin: OAUTH_CALLBACK_ORIGIN,
354
+ callbackUri: OAUTH_CALLBACK_URI,
355
+ callbackPath: OAUTH_CALLBACK_PATH,
356
+ callbackTimeoutMs: OAUTH_CALLBACK_TIMEOUT_MS,
357
+ buildOAuthErrorHtml,
358
+ buildOAuthSuccessHtml,
359
+ composeCodexSuccessRedirectUrl,
360
+ exchangeCodeForTokens
361
+ });
381
362
  function isOAuthDebugEnabled() {
382
- const raw = process.env.CODEX_AUTH_DEBUG?.trim().toLowerCase();
383
- return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
384
- }
385
- function emitOAuthDebug(event, meta = {}) {
386
- if (!isOAuthDebugEnabled())
387
- return;
388
- const payload = {
389
- ts: new Date().toISOString(),
390
- pid: process.pid,
391
- event,
392
- ...meta
393
- };
394
- const line = JSON.stringify(payload);
395
- try {
396
- console.error(`[codex-auth-debug] ${line}`);
397
- }
398
- catch {
399
- // best effort stderr logging
400
- }
401
- try {
402
- mkdirSync(OAUTH_DEBUG_LOG_DIR, { recursive: true, mode: 0o700 });
403
- appendFileSync(OAUTH_DEBUG_LOG_FILE, `${line}\n`, { encoding: "utf8", mode: 0o600 });
404
- chmodSync(OAUTH_DEBUG_LOG_FILE, 0o600);
405
- }
406
- catch {
407
- // best effort file logging
408
- }
409
- }
410
- function clearOAuthServerCloseTimer() {
411
- if (!oauthServerCloseTimer)
412
- return;
413
- clearTimeout(oauthServerCloseTimer);
414
- oauthServerCloseTimer = undefined;
415
- }
416
- function isLoopbackRemoteAddress(remoteAddress) {
417
- if (!remoteAddress)
418
- return false;
419
- const normalized = remoteAddress.split("%")[0]?.toLowerCase();
420
- return normalized === "127.0.0.1" || normalized === "::1" || normalized === "::ffff:127.0.0.1";
421
- }
422
- function setOAuthResponseHeaders(res, options) {
423
- res.setHeader("Cache-Control", "no-store");
424
- res.setHeader("Pragma", "no-cache");
425
- res.setHeader("Referrer-Policy", "no-referrer");
426
- res.setHeader("X-Content-Type-Options", "nosniff");
427
- if (options?.contentType) {
428
- res.setHeader("Content-Type", options.contentType);
429
- }
430
- if (options?.isHtml) {
431
- res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'");
432
- }
363
+ return oauthServerController.isDebugEnabled();
433
364
  }
434
365
  async function startOAuthServer() {
435
- clearOAuthServerCloseTimer();
436
- if (oauthServer) {
437
- emitOAuthDebug("server_reuse", { port: OAUTH_PORT });
438
- return { redirectUri: OAUTH_CALLBACK_URI };
439
- }
440
- emitOAuthDebug("server_starting", { port: OAUTH_PORT });
441
- oauthServer = http.createServer((req, res) => {
442
- try {
443
- if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
444
- emitOAuthDebug("callback_rejected_non_loopback", {
445
- remoteAddress: req.socket.remoteAddress
446
- });
447
- res.statusCode = 403;
448
- setOAuthResponseHeaders(res, { contentType: "text/plain; charset=utf-8" });
449
- res.end("Forbidden");
450
- return;
451
- }
452
- const url = new URL(req.url ?? "/", OAUTH_CALLBACK_ORIGIN);
453
- const sendHtml = (status, html) => {
454
- res.statusCode = status;
455
- setOAuthResponseHeaders(res, { contentType: "text/html; charset=utf-8", isHtml: true });
456
- res.end(html);
457
- };
458
- const redirect = (location) => {
459
- res.statusCode = 302;
460
- setOAuthResponseHeaders(res, { contentType: "text/plain; charset=utf-8" });
461
- res.setHeader("Location", location);
462
- res.end();
463
- };
464
- if (url.pathname === "/auth/callback") {
465
- const code = url.searchParams.get("code");
466
- const state = url.searchParams.get("state");
467
- const error = url.searchParams.get("error");
468
- const errorDescription = url.searchParams.get("error_description");
469
- emitOAuthDebug("callback_hit", {
470
- hasCode: Boolean(code),
471
- hasState: Boolean(state),
472
- hasError: Boolean(error)
473
- });
474
- if (error) {
475
- const errorMsg = errorDescription || error;
476
- emitOAuthDebug("callback_error", { reason: errorMsg });
477
- pendingOAuth?.reject(new Error(errorMsg));
478
- pendingOAuth = undefined;
479
- sendHtml(200, buildOAuthErrorHtml(errorMsg));
480
- return;
481
- }
482
- if (!code) {
483
- const errorMsg = "Missing authorization code";
484
- emitOAuthDebug("callback_error", { reason: errorMsg });
485
- pendingOAuth?.reject(new Error(errorMsg));
486
- pendingOAuth = undefined;
487
- sendHtml(400, buildOAuthErrorHtml(errorMsg));
488
- return;
489
- }
490
- if (!pendingOAuth || state !== pendingOAuth.state) {
491
- const errorMsg = "Invalid state - potential CSRF attack";
492
- emitOAuthDebug("callback_error", { reason: errorMsg });
493
- pendingOAuth?.reject(new Error(errorMsg));
494
- pendingOAuth = undefined;
495
- sendHtml(400, buildOAuthErrorHtml(errorMsg));
496
- return;
497
- }
498
- const current = pendingOAuth;
499
- pendingOAuth = undefined;
500
- emitOAuthDebug("token_exchange_start", { authMode: current.authMode });
501
- exchangeCodeForTokens(code, OAUTH_CALLBACK_URI, current.pkce)
502
- .then((tokens) => {
503
- current.resolve(tokens);
504
- emitOAuthDebug("token_exchange_success", { authMode: current.authMode });
505
- if (res.writableEnded)
506
- return;
507
- if (current.authMode === "codex") {
508
- redirect(composeCodexSuccessRedirectUrl(tokens));
509
- return;
510
- }
511
- sendHtml(200, buildOAuthSuccessHtml("native"));
512
- })
513
- .catch((err) => {
514
- const oauthError = err instanceof Error ? err : new Error(String(err));
515
- current.reject(oauthError);
516
- emitOAuthDebug("token_exchange_error", { error: oauthError.message });
517
- if (res.writableEnded)
518
- return;
519
- sendHtml(500, buildOAuthErrorHtml(oauthError.message));
520
- });
521
- return;
522
- }
523
- if (url.pathname === "/success") {
524
- emitOAuthDebug("callback_success_page");
525
- sendHtml(200, buildOAuthSuccessHtml("codex"));
526
- return;
527
- }
528
- if (url.pathname === "/cancel") {
529
- emitOAuthDebug("callback_cancel");
530
- pendingOAuth?.reject(new Error("Login cancelled"));
531
- pendingOAuth = undefined;
532
- res.statusCode = 200;
533
- setOAuthResponseHeaders(res, { contentType: "text/plain; charset=utf-8" });
534
- res.end("Login cancelled");
535
- return;
536
- }
537
- res.statusCode = 404;
538
- setOAuthResponseHeaders(res, { contentType: "text/plain; charset=utf-8" });
539
- res.end("Not found");
540
- }
541
- catch (error) {
542
- res.statusCode = 500;
543
- setOAuthResponseHeaders(res, { contentType: "text/plain; charset=utf-8" });
544
- res.end(`Server error: ${error.message}`);
545
- }
546
- });
547
- try {
548
- await new Promise((resolve, reject) => {
549
- oauthServer?.once("error", reject);
550
- oauthServer?.listen(OAUTH_PORT, OAUTH_LOOPBACK_HOST, () => resolve());
551
- });
552
- emitOAuthDebug("server_started", { port: OAUTH_PORT });
553
- }
554
- catch (error) {
555
- emitOAuthDebug("server_start_error", {
556
- error: error instanceof Error ? error.message : String(error)
557
- });
558
- const server = oauthServer;
559
- oauthServer = undefined;
560
- try {
561
- server?.close();
562
- }
563
- catch {
564
- // best-effort cleanup
565
- }
566
- throw error;
567
- }
568
- return { redirectUri: OAUTH_CALLBACK_URI };
366
+ return oauthServerController.start();
569
367
  }
570
368
  function stopOAuthServer() {
571
- clearOAuthServerCloseTimer();
572
- emitOAuthDebug("server_stopping", { hadPendingOAuth: Boolean(pendingOAuth) });
573
- oauthServer?.close();
574
- oauthServer = undefined;
575
- emitOAuthDebug("server_stopped");
369
+ oauthServerController.stop();
576
370
  }
577
371
  function scheduleOAuthServerStop(delayMs = OAUTH_SERVER_SHUTDOWN_GRACE_MS, reason = "other") {
578
- if (!oauthServer)
579
- return;
580
- clearOAuthServerCloseTimer();
581
- emitOAuthDebug("server_stop_scheduled", { delayMs, reason });
582
- oauthServerCloseTimer = setTimeout(() => {
583
- oauthServerCloseTimer = undefined;
584
- if (pendingOAuth)
585
- return;
586
- emitOAuthDebug("server_stop_timer_fired", { reason });
587
- stopOAuthServer();
588
- }, delayMs);
372
+ oauthServerController.scheduleStop(delayMs, reason);
589
373
  }
590
374
  function waitForOAuthCallback(pkce, state, authMode) {
591
- emitOAuthDebug("callback_wait_start", {
592
- authMode,
593
- stateTail: state.slice(-6)
594
- });
595
- return new Promise((resolve, reject) => {
596
- let settled = false;
597
- const resolveOnce = (tokens) => {
598
- if (settled)
599
- return;
600
- settled = true;
601
- clearTimeout(timeout);
602
- emitOAuthDebug("callback_wait_resolved", { authMode });
603
- resolve(tokens);
604
- };
605
- const rejectOnce = (error) => {
606
- if (settled)
607
- return;
608
- settled = true;
609
- clearTimeout(timeout);
610
- emitOAuthDebug("callback_wait_rejected", { authMode, error: error.message });
611
- reject(error);
612
- };
613
- const timeout = setTimeout(() => {
614
- pendingOAuth = undefined;
615
- emitOAuthDebug("callback_wait_timeout", { authMode, timeoutMs: OAUTH_CALLBACK_TIMEOUT_MS });
616
- rejectOnce(new Error("OAuth callback timeout - authorization took too long"));
617
- }, OAUTH_CALLBACK_TIMEOUT_MS);
618
- pendingOAuth = {
619
- pkce,
620
- state,
621
- authMode,
622
- resolve: resolveOnce,
623
- reject: rejectOnce
624
- };
625
- });
375
+ return oauthServerController.waitForCallback(pkce, state, authMode);
626
376
  }
627
377
  function modeForRuntimeMode(runtimeMode) {
628
378
  return runtimeMode === "native" ? "native" : "codex";
@@ -711,319 +461,38 @@ function rewriteUrl(requestInput) {
711
461
  const parsed = requestInput instanceof URL
712
462
  ? requestInput
713
463
  : new URL(typeof requestInput === "string" ? requestInput : requestInput.url);
714
- if (parsed.pathname.includes("/v1/responses") ||
715
- parsed.pathname.includes("/chat/completions")) {
464
+ if (parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions")) {
716
465
  return new URL(CODEX_API_ENDPOINT);
717
466
  }
718
467
  return parsed;
719
468
  }
720
- function opencodeUserAgent() {
721
- const version = resolvePluginVersion();
722
- return `opencode/${version} (${os.platform()} ${os.release()}; ${os.arch()})`;
723
- }
724
- let cachedPluginVersion;
725
- let cachedMacProductVersion;
726
- let cachedTerminalUserAgentToken;
727
- function isPrintableAscii(value) {
728
- if (!value)
729
- return false;
730
- for (let index = 0; index < value.length; index += 1) {
731
- const code = value.charCodeAt(index);
732
- if (code < 0x20 || code > 0x7e)
733
- return false;
734
- }
735
- return true;
736
- }
737
- function sanitizeUserAgentCandidate(candidate, fallback, originator) {
738
- if (isPrintableAscii(candidate))
739
- return candidate;
740
- const sanitized = Array.from(candidate)
741
- .map((char) => {
742
- const code = char.charCodeAt(0);
743
- return code >= 0x20 && code <= 0x7e ? char : "_";
744
- })
745
- .join("");
746
- if (isPrintableAscii(sanitized))
747
- return sanitized;
748
- if (isPrintableAscii(fallback))
749
- return fallback;
750
- return originator;
751
- }
752
- function sanitizeTerminalToken(value) {
753
- return value.replace(/[^A-Za-z0-9._/-]/g, "_");
754
- }
755
- function nonEmptyEnv(env, key) {
756
- const value = env[key]?.trim();
757
- return value ? value : undefined;
758
- }
759
- function splitProgramAndVersion(value) {
760
- const [program, version] = value.trim().split(/\s+/, 2);
761
- return {
762
- program: program ?? "unknown",
763
- ...(version ? { version } : {})
764
- };
765
- }
766
- function tmuxDisplayMessage(format) {
767
- try {
768
- const value = execFileSync("tmux", ["display-message", "-p", format], { encoding: "utf8" }).trim();
769
- return value || undefined;
770
- }
771
- catch {
772
- return undefined;
773
- }
774
- }
775
- function resolveTerminalUserAgentToken(env = process.env) {
776
- if (cachedTerminalUserAgentToken)
777
- return cachedTerminalUserAgentToken;
778
- const termProgram = nonEmptyEnv(env, "TERM_PROGRAM");
779
- const termProgramVersion = nonEmptyEnv(env, "TERM_PROGRAM_VERSION");
780
- const term = nonEmptyEnv(env, "TERM");
781
- const hasTmux = Boolean(nonEmptyEnv(env, "TMUX") || nonEmptyEnv(env, "TMUX_PANE"));
782
- if (termProgram && termProgram.toLowerCase() === "tmux" && hasTmux) {
783
- const tmuxTermType = tmuxDisplayMessage("#{client_termtype}");
784
- if (tmuxTermType) {
785
- const { program, version } = splitProgramAndVersion(tmuxTermType);
786
- cachedTerminalUserAgentToken = sanitizeTerminalToken(version ? `${program}/${version}` : program);
787
- return cachedTerminalUserAgentToken;
788
- }
789
- const tmuxTermName = tmuxDisplayMessage("#{client_termname}");
790
- if (tmuxTermName) {
791
- cachedTerminalUserAgentToken = sanitizeTerminalToken(tmuxTermName);
792
- return cachedTerminalUserAgentToken;
793
- }
794
- }
795
- if (termProgram) {
796
- cachedTerminalUserAgentToken = sanitizeTerminalToken(termProgramVersion ? `${termProgram}/${termProgramVersion}` : termProgram);
797
- return cachedTerminalUserAgentToken;
798
- }
799
- const weztermVersion = nonEmptyEnv(env, "WEZTERM_VERSION");
800
- if (weztermVersion) {
801
- cachedTerminalUserAgentToken = sanitizeTerminalToken(`WezTerm/${weztermVersion}`);
802
- return cachedTerminalUserAgentToken;
803
- }
804
- if (env.ITERM_SESSION_ID || env.ITERM_PROFILE || env.ITERM_PROFILE_NAME) {
805
- cachedTerminalUserAgentToken = "iTerm.app";
806
- return cachedTerminalUserAgentToken;
807
- }
808
- if (env.TERM_SESSION_ID) {
809
- cachedTerminalUserAgentToken = "Apple_Terminal";
810
- return cachedTerminalUserAgentToken;
811
- }
812
- if (env.KITTY_WINDOW_ID || term?.includes("kitty")) {
813
- cachedTerminalUserAgentToken = "kitty";
814
- return cachedTerminalUserAgentToken;
815
- }
816
- if (env.ALACRITTY_SOCKET || term === "alacritty") {
817
- cachedTerminalUserAgentToken = "Alacritty";
818
- return cachedTerminalUserAgentToken;
819
- }
820
- const konsoleVersion = nonEmptyEnv(env, "KONSOLE_VERSION");
821
- if (konsoleVersion) {
822
- cachedTerminalUserAgentToken = sanitizeTerminalToken(`Konsole/${konsoleVersion}`);
823
- return cachedTerminalUserAgentToken;
824
- }
825
- if (env.GNOME_TERMINAL_SCREEN) {
826
- cachedTerminalUserAgentToken = "gnome-terminal";
827
- return cachedTerminalUserAgentToken;
828
- }
829
- const vteVersion = nonEmptyEnv(env, "VTE_VERSION");
830
- if (vteVersion) {
831
- cachedTerminalUserAgentToken = sanitizeTerminalToken(`VTE/${vteVersion}`);
832
- return cachedTerminalUserAgentToken;
833
- }
834
- if (env.WT_SESSION) {
835
- cachedTerminalUserAgentToken = "WindowsTerminal";
836
- return cachedTerminalUserAgentToken;
837
- }
838
- if (term) {
839
- cachedTerminalUserAgentToken = sanitizeTerminalToken(term);
840
- return cachedTerminalUserAgentToken;
841
- }
842
- cachedTerminalUserAgentToken = "unknown";
843
- return cachedTerminalUserAgentToken;
844
- }
845
- function resolvePluginVersion() {
846
- if (cachedPluginVersion)
847
- return cachedPluginVersion;
848
- const fromEnv = process.env.npm_package_version?.trim();
849
- if (fromEnv) {
850
- cachedPluginVersion = fromEnv;
851
- return cachedPluginVersion;
852
- }
853
- try {
854
- const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
855
- const parsed = JSON.parse(raw);
856
- if (typeof parsed.version === "string" && parsed.version.trim()) {
857
- cachedPluginVersion = parsed.version.trim();
858
- return cachedPluginVersion;
859
- }
860
- }
861
- catch {
862
- // Use fallback version below.
863
- }
864
- cachedPluginVersion = DEFAULT_PLUGIN_VERSION;
865
- return cachedPluginVersion;
866
- }
867
- function resolveMacProductVersion() {
868
- if (cachedMacProductVersion)
869
- return cachedMacProductVersion;
870
- try {
871
- const value = execFileSync("sw_vers", ["-productVersion"], { encoding: "utf8" }).trim();
872
- cachedMacProductVersion = value || os.release();
873
- }
874
- catch {
875
- cachedMacProductVersion = os.release();
876
- }
877
- return cachedMacProductVersion;
878
- }
879
- function normalizeArchitecture(architecture) {
880
- if (architecture === "x64")
881
- return "x86_64";
882
- if (architecture === "arm64")
883
- return "arm64";
884
- return architecture || "unknown";
885
- }
886
- function resolveCodexPlatformSignature(platform = process.platform) {
887
- const architecture = normalizeArchitecture(os.arch());
888
- if (platform === "darwin") {
889
- return `Mac OS ${resolveMacProductVersion()}; ${architecture}`;
890
- }
891
- if (platform === "win32") {
892
- return `Windows ${os.release()}; ${architecture}`;
893
- }
894
- if (platform === "linux") {
895
- return `Linux ${os.release()}; ${architecture}`;
896
- }
897
- return `${platform} ${os.release()}; ${architecture}`;
898
- }
899
- function buildCodexUserAgent(originator) {
900
- if (originator === "opencode")
901
- return opencodeUserAgent();
902
- const buildVersion = resolvePluginVersion();
903
- const terminalToken = resolveTerminalUserAgentToken();
904
- const prefix = `${originator}/${buildVersion} (${resolveCodexPlatformSignature()}) ${terminalToken}`;
905
- return sanitizeUserAgentCandidate(prefix, prefix, originator);
906
- }
907
- function resolveRequestUserAgent(spoofMode, originator) {
908
- if (spoofMode === "codex")
909
- return buildCodexUserAgent(originator);
910
- return opencodeUserAgent();
911
- }
912
- function resolveHookAgentName(agent) {
913
- const direct = asString(agent);
914
- if (direct)
915
- return direct;
916
- if (!isRecord(agent))
917
- return undefined;
918
- return asString(agent.name) ?? asString(agent.agent);
919
- }
920
- function normalizeAgentNameForCollaboration(agentName) {
921
- return agentName.trim().toLowerCase().replace(/\s+/g, "-");
922
- }
923
- function tokenizeAgentName(normalizedAgentName) {
924
- return normalizedAgentName
925
- .split(/[-./:_]+/)
926
- .map((token) => token.trim())
927
- .filter((token) => token.length > 0);
928
- }
929
- function isPluginCollaborationAgent(normalizedAgentName) {
930
- const tokens = tokenizeAgentName(normalizedAgentName);
931
- if (tokens.length === 0)
932
- return false;
933
- if (tokens[0] !== "codex")
469
+ function isAllowedOpenAIOutboundHost(hostname) {
470
+ const normalized = hostname.trim().toLowerCase();
471
+ if (!normalized)
934
472
  return false;
935
- return tokens.some((token) => [
936
- "orchestrator",
937
- "default",
938
- "code",
939
- "plan",
940
- "planner",
941
- "execute",
942
- "pair",
943
- "pairprogramming",
944
- "review",
945
- "compact",
946
- "compaction"
947
- ].includes(token));
948
- }
949
- function resolveCollaborationModeKindFromName(normalizedAgentName) {
950
- const tokens = tokenizeAgentName(normalizedAgentName);
951
- if (tokens.includes("plan") || tokens.includes("planner"))
952
- return "plan";
953
- if (tokens.includes("execute"))
954
- return "execute";
955
- if (tokens.includes("pair") || tokens.includes("pairprogramming"))
956
- return "pair_programming";
957
- return "code";
958
- }
959
- function resolveCollaborationProfile(agent) {
960
- const name = resolveHookAgentName(agent);
961
- if (!name)
962
- return { enabled: false };
963
- const normalizedAgentName = normalizeAgentNameForCollaboration(name);
964
- if (!isPluginCollaborationAgent(normalizedAgentName)) {
965
- return { enabled: false, normalizedAgentName };
966
- }
967
- return {
968
- enabled: true,
969
- normalizedAgentName,
970
- kind: resolveCollaborationModeKindFromName(normalizedAgentName)
971
- };
972
- }
973
- function resolveCollaborationModeKind(agent) {
974
- const profile = resolveCollaborationProfile(agent);
975
- return profile.kind ?? "code";
976
- }
977
- function resolveCollaborationInstructions(kind) {
978
- if (kind === "plan")
979
- return CODEX_PLAN_MODE_INSTRUCTIONS;
980
- if (kind === "execute")
981
- return CODEX_EXECUTE_MODE_INSTRUCTIONS;
982
- if (kind === "pair_programming")
983
- return CODEX_PAIR_PROGRAMMING_MODE_INSTRUCTIONS;
984
- return CODEX_CODE_MODE_INSTRUCTIONS;
985
- }
986
- function mergeInstructions(base, extra) {
987
- const normalizedExtra = extra.trim();
988
- if (!normalizedExtra)
989
- return base?.trim() ?? "";
990
- const normalizedBase = base?.trim();
991
- if (!normalizedBase)
992
- return normalizedExtra;
993
- if (normalizedBase.includes(normalizedExtra))
994
- return normalizedBase;
995
- return `${normalizedBase}\n\n${normalizedExtra}`;
996
- }
997
- function resolveSubagentHeaderValue(agent) {
998
- const profile = resolveCollaborationProfile(agent);
999
- const normalized = profile.normalizedAgentName;
1000
- if (!profile.enabled || !normalized) {
1001
- return undefined;
1002
- }
1003
- const tokens = tokenizeAgentName(normalized);
1004
- const isCodexPrimary = tokens[0] === "codex" &&
1005
- (tokens.includes("orchestrator") ||
1006
- tokens.includes("default") ||
1007
- tokens.includes("code") ||
1008
- tokens.includes("plan") ||
1009
- tokens.includes("planner") ||
1010
- tokens.includes("execute") ||
1011
- tokens.includes("pair") ||
1012
- tokens.includes("pairprogramming"));
1013
- if (isCodexPrimary) {
1014
- return undefined;
1015
- }
1016
- if (tokens.includes("plan") || tokens.includes("planner")) {
1017
- return undefined;
1018
- }
1019
- if (normalized === "compaction") {
1020
- return "compact";
473
+ if (OPENAI_OUTBOUND_HOST_ALLOWLIST.has(normalized))
474
+ return true;
475
+ return normalized.endsWith(".openai.com") || normalized.endsWith(".chatgpt.com");
476
+ }
477
+ function assertAllowedOutboundUrl(url) {
478
+ const protocol = url.protocol.trim().toLowerCase();
479
+ if (protocol !== "https:") {
480
+ throw new PluginFatalError({
481
+ message: `Blocked outbound request with unsupported protocol "${protocol || "unknown"}". ` +
482
+ "This plugin only proxies HTTPS requests to OpenAI/ChatGPT backends.",
483
+ status: 400,
484
+ type: "disallowed_outbound_protocol",
485
+ param: "request"
486
+ });
1021
487
  }
1022
- if (normalized.includes("review"))
1023
- return "review";
1024
- if (normalized.includes("compact") || normalized.includes("compaction"))
1025
- return "compact";
1026
- return "collab_spawn";
488
+ if (isAllowedOpenAIOutboundHost(url.hostname))
489
+ return;
490
+ throw new PluginFatalError({
491
+ message: `Blocked outbound request to "${url.hostname}". ` + "This plugin only proxies OpenAI/ChatGPT backend traffic.",
492
+ status: 400,
493
+ type: "disallowed_outbound_host",
494
+ param: "request"
495
+ });
1027
496
  }
1028
497
  async function sessionUsesOpenAIProvider(client, sessionID) {
1029
498
  const sessionApi = client?.session;
@@ -1040,9 +509,7 @@ async function sessionUsesOpenAIProvider(client, sessionID) {
1040
509
  if (asString(info.role) !== "user")
1041
510
  continue;
1042
511
  const model = isRecord(info.model) ? info.model : undefined;
1043
- const providerID = model
1044
- ? asString(model.providerID)
1045
- : asString(info.providerID);
512
+ const providerID = model ? asString(model.providerID) : asString(info.providerID);
1046
513
  if (!providerID)
1047
514
  continue;
1048
515
  return providerID === "openai";
@@ -1053,26 +520,11 @@ async function sessionUsesOpenAIProvider(client, sessionID) {
1053
520
  }
1054
521
  return false;
1055
522
  }
1056
- function isTuiWorkerInvocation(argv) {
1057
- return argv.some((entry) => /(?:^|[\\/])tui[\\/]worker\.(?:js|ts)$/i.test(entry));
1058
- }
1059
- function resolveCodexOriginator(spoofMode, argv = process.argv) {
1060
- if (spoofMode !== "codex")
1061
- return "opencode";
1062
- const normalizedArgv = argv.map((entry) => String(entry));
1063
- if (isTuiWorkerInvocation(normalizedArgv))
1064
- return "codex_cli_rs";
1065
- return normalizedArgv.includes("run") ? "codex_exec" : "codex_cli_rs";
1066
- }
1067
523
  function formatAccountLabel(account, index) {
1068
524
  const email = account?.email?.trim();
1069
525
  const plan = account?.plan?.trim();
1070
526
  const accountId = account?.accountId?.trim();
1071
- const idSuffix = accountId
1072
- ? accountId.length > 6
1073
- ? accountId.slice(-6)
1074
- : accountId
1075
- : null;
527
+ const idSuffix = accountId ? (accountId.length > 6 ? accountId.slice(-6) : accountId) : null;
1076
528
  if (email && plan)
1077
529
  return `${email} (${plan})`;
1078
530
  if (email)
@@ -1082,7 +534,7 @@ function formatAccountLabel(account, index) {
1082
534
  return `Account ${index + 1}`;
1083
535
  }
1084
536
  function hasActiveCooldown(account, now) {
1085
- return typeof account.cooldownUntil === "number" && Number.isFinite(account.cooldownUntil) && account.cooldownUntil > now;
537
+ return (typeof account.cooldownUntil === "number" && Number.isFinite(account.cooldownUntil) && account.cooldownUntil > now);
1086
538
  }
1087
539
  function ensureAccountAuthTypes(account) {
1088
540
  const normalized = normalizeAccountAuthTypes(account.authTypes);
@@ -1128,13 +580,12 @@ function buildAuthMenuAccounts(input) {
1128
580
  const existing = rows.get(identity);
1129
581
  const currentStatus = hasActiveCooldown(account, now)
1130
582
  ? "rate-limited"
1131
- : typeof account.expires === "number" &&
1132
- Number.isFinite(account.expires) &&
1133
- account.expires <= now
583
+ : typeof account.expires === "number" && Number.isFinite(account.expires) && account.expires <= now
1134
584
  ? "expired"
1135
585
  : "unknown";
1136
586
  if (!existing) {
1137
- const isCurrentAccount = authMode === input.activeMode && Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
587
+ const isCurrentAccount = authMode === input.activeMode &&
588
+ Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
1138
589
  rows.set(identity, {
1139
590
  identityKey: account.identityKey,
1140
591
  index: rows.size,
@@ -1150,8 +601,7 @@ function buildAuthMenuAccounts(input) {
1150
601
  continue;
1151
602
  }
1152
603
  existing.authTypes = normalizeAccountAuthTypes([...(existing.authTypes ?? []), authMode]);
1153
- if (typeof account.lastUsed === "number" &&
1154
- (!existing.lastUsed || account.lastUsed > existing.lastUsed)) {
604
+ if (typeof account.lastUsed === "number" && (!existing.lastUsed || account.lastUsed > existing.lastUsed)) {
1155
605
  existing.lastUsed = account.lastUsed;
1156
606
  }
1157
607
  if (existing.enabled === false && account.enabled !== false) {
@@ -1160,12 +610,11 @@ function buildAuthMenuAccounts(input) {
1160
610
  if (existing.status !== "rate-limited" && currentStatus === "rate-limited") {
1161
611
  existing.status = "rate-limited";
1162
612
  }
1163
- else if (existing.status !== "rate-limited" &&
1164
- existing.status !== "expired" &&
1165
- currentStatus === "expired") {
613
+ else if (existing.status !== "rate-limited" && existing.status !== "expired" && currentStatus === "expired") {
1166
614
  existing.status = "expired";
1167
615
  }
1168
- const isCurrentAccount = authMode === input.activeMode && Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
616
+ const isCurrentAccount = authMode === input.activeMode &&
617
+ Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
1169
618
  if (isCurrentAccount) {
1170
619
  existing.isCurrentAccount = true;
1171
620
  existing.status = "active";
@@ -1177,9 +626,7 @@ function buildAuthMenuAccounts(input) {
1177
626
  return Array.from(rows.values()).map((row, index) => ({ ...row, index }));
1178
627
  }
1179
628
  function hydrateAccountIdentityFromAccessClaims(account) {
1180
- const claims = typeof account.access === "string" && account.access.length > 0
1181
- ? parseJwtClaims(account.access)
1182
- : undefined;
629
+ const claims = typeof account.access === "string" && account.access.length > 0 ? parseJwtClaims(account.access) : undefined;
1183
630
  if (!account.accountId)
1184
631
  account.accountId = extractAccountIdFromClaims(claims);
1185
632
  if (!account.email)
@@ -1193,35 +640,6 @@ function hydrateAccountIdentityFromAccessClaims(account) {
1193
640
  ensureAccountAuthTypes(account);
1194
641
  synchronizeIdentityKey(account);
1195
642
  }
1196
- async function selectCatalogAuthCandidate(authMode, pidOffsetEnabled, rotationStrategy) {
1197
- try {
1198
- const auth = await loadAuthStorage();
1199
- const domain = getOpenAIOAuthDomain(auth, authMode);
1200
- if (!domain) {
1201
- return {};
1202
- }
1203
- const selected = selectAccount({
1204
- accounts: domain.accounts,
1205
- strategy: rotationStrategy ?? domain.strategy,
1206
- activeIdentityKey: domain.activeIdentityKey,
1207
- now: Date.now(),
1208
- stickyPidOffset: pidOffsetEnabled
1209
- });
1210
- if (!selected?.access) {
1211
- return { accountId: selected?.accountId };
1212
- }
1213
- if (selected.expires && selected.expires <= Date.now()) {
1214
- return { accountId: selected.accountId };
1215
- }
1216
- return {
1217
- accessToken: selected.access,
1218
- accountId: selected.accountId
1219
- };
1220
- }
1221
- catch {
1222
- return {};
1223
- }
1224
- }
1225
643
  function isRecord(value) {
1226
644
  return typeof value === "object" && value !== null && !Array.isArray(value);
1227
645
  }
@@ -1231,352 +649,9 @@ function asString(value) {
1231
649
  const trimmed = value.trim();
1232
650
  return trimmed ? trimmed : undefined;
1233
651
  }
1234
- function asStringArray(value) {
1235
- if (!Array.isArray(value))
1236
- return undefined;
1237
- return value.filter((item) => typeof item === "string" && item.trim().length > 0);
1238
- }
1239
- function normalizeReasoningSummaryOption(value) {
1240
- const normalized = asString(value)?.toLowerCase();
1241
- if (!normalized || normalized === "none")
1242
- return undefined;
1243
- if (normalized === "auto" || normalized === "concise" || normalized === "detailed")
1244
- return normalized;
1245
- return undefined;
1246
- }
1247
- function readModelRuntimeDefaults(options) {
1248
- const raw = options.codexRuntimeDefaults;
1249
- if (!isRecord(raw))
1250
- return {};
1251
- return {
1252
- applyPatchToolType: asString(raw.applyPatchToolType),
1253
- defaultReasoningEffort: asString(raw.defaultReasoningEffort),
1254
- supportsReasoningSummaries: typeof raw.supportsReasoningSummaries === "boolean" ? raw.supportsReasoningSummaries : undefined,
1255
- reasoningSummaryFormat: asString(raw.reasoningSummaryFormat),
1256
- defaultVerbosity: raw.defaultVerbosity === "low" || raw.defaultVerbosity === "medium" || raw.defaultVerbosity === "high"
1257
- ? raw.defaultVerbosity
1258
- : undefined,
1259
- supportsVerbosity: typeof raw.supportsVerbosity === "boolean" ? raw.supportsVerbosity : undefined
1260
- };
1261
- }
1262
- function mergeUnique(values) {
1263
- const out = [];
1264
- const seen = new Set();
1265
- for (const value of values) {
1266
- if (seen.has(value))
1267
- continue;
1268
- seen.add(value);
1269
- out.push(value);
1270
- }
1271
- return out;
1272
- }
1273
- function normalizePersonalityKey(value) {
1274
- const normalized = asString(value)?.toLowerCase();
1275
- if (!normalized)
1276
- return undefined;
1277
- if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("..")) {
1278
- return undefined;
1279
- }
1280
- return normalized;
1281
- }
1282
- function getModelLookupCandidates(model) {
1283
- const out = [];
1284
- const seen = new Set();
1285
- const add = (value) => {
1286
- const trimmed = value?.trim();
1287
- if (!trimmed)
1288
- return;
1289
- if (seen.has(trimmed))
1290
- return;
1291
- seen.add(trimmed);
1292
- out.push(trimmed);
1293
- };
1294
- add(model.id);
1295
- add(model.api?.id);
1296
- add(model.id?.split("/").pop());
1297
- add(model.api?.id?.split("/").pop());
1298
- return out;
1299
- }
1300
- function getVariantLookupCandidates(input) {
1301
- const out = [];
1302
- const seen = new Set();
1303
- const add = (value) => {
1304
- const trimmed = value?.trim();
1305
- if (!trimmed)
1306
- return;
1307
- if (seen.has(trimmed))
1308
- return;
1309
- seen.add(trimmed);
1310
- out.push(trimmed);
1311
- };
1312
- if (isRecord(input.message)) {
1313
- add(asString(input.message.variant));
1314
- }
1315
- for (const candidate of input.modelCandidates) {
1316
- const slash = candidate.lastIndexOf("/");
1317
- if (slash <= 0 || slash >= candidate.length - 1)
1318
- continue;
1319
- add(candidate.slice(slash + 1));
1320
- }
1321
- return out;
1322
- }
1323
- const EFFORT_SUFFIX_REGEX = /-(none|minimal|low|medium|high|xhigh)$/i;
1324
- function stripEffortSuffix(value) {
1325
- return value.replace(EFFORT_SUFFIX_REGEX, "");
1326
- }
1327
- function findCatalogModelForCandidates(catalogModels, modelCandidates) {
1328
- if (!catalogModels || catalogModels.length === 0)
1329
- return undefined;
1330
- const wanted = new Set();
1331
- for (const candidate of modelCandidates) {
1332
- const normalized = candidate.trim().toLowerCase();
1333
- if (!normalized)
1334
- continue;
1335
- wanted.add(normalized);
1336
- wanted.add(stripEffortSuffix(normalized));
1337
- }
1338
- return catalogModels.find((model) => {
1339
- const slug = model.slug.trim().toLowerCase();
1340
- if (!slug)
1341
- return false;
1342
- return wanted.has(slug) || wanted.has(stripEffortSuffix(slug));
1343
- });
1344
- }
1345
- function resolveCaseInsensitiveEntry(entries, candidate) {
1346
- if (!entries)
1347
- return undefined;
1348
- const direct = entries[candidate];
1349
- if (direct !== undefined)
1350
- return direct;
1351
- const lowered = entries[candidate.toLowerCase()];
1352
- if (lowered !== undefined)
1353
- return lowered;
1354
- const loweredCandidate = candidate.toLowerCase();
1355
- for (const [name, entry] of Object.entries(entries)) {
1356
- if (name.trim().toLowerCase() === loweredCandidate) {
1357
- return entry;
1358
- }
1359
- }
1360
- return undefined;
1361
- }
1362
- function getModelPersonalityOverride(customSettings, modelCandidates, variantCandidates) {
1363
- const models = customSettings?.models;
1364
- if (!models)
1365
- return undefined;
1366
- for (const candidate of modelCandidates) {
1367
- const entry = resolveCaseInsensitiveEntry(models, candidate);
1368
- if (!entry)
1369
- continue;
1370
- for (const variantCandidate of variantCandidates) {
1371
- const variantEntry = resolveCaseInsensitiveEntry(entry.variants, variantCandidate);
1372
- const variantPersonality = normalizePersonalityKey(variantEntry?.options?.personality);
1373
- if (variantPersonality)
1374
- return variantPersonality;
1375
- }
1376
- const modelPersonality = normalizePersonalityKey(entry.options?.personality);
1377
- if (modelPersonality)
1378
- return modelPersonality;
1379
- }
1380
- return undefined;
1381
- }
1382
- function getModelThinkingSummariesOverride(customSettings, modelCandidates, variantCandidates) {
1383
- const models = customSettings?.models;
1384
- if (!models)
1385
- return undefined;
1386
- for (const candidate of modelCandidates) {
1387
- const entry = resolveCaseInsensitiveEntry(models, candidate);
1388
- if (!entry)
1389
- continue;
1390
- for (const variantCandidate of variantCandidates) {
1391
- const variantEntry = resolveCaseInsensitiveEntry(entry.variants, variantCandidate);
1392
- if (typeof variantEntry?.thinkingSummaries === "boolean") {
1393
- return variantEntry.thinkingSummaries;
1394
- }
1395
- }
1396
- if (typeof entry.thinkingSummaries === "boolean") {
1397
- return entry.thinkingSummaries;
1398
- }
1399
- }
1400
- return undefined;
1401
- }
1402
- function resolvePersonalityForModel(input) {
1403
- const modelOverride = getModelPersonalityOverride(input.customSettings, input.modelCandidates, input.variantCandidates);
1404
- if (modelOverride)
1405
- return modelOverride;
1406
- const globalOverride = normalizePersonalityKey(input.customSettings?.options?.personality);
1407
- if (globalOverride)
1408
- return globalOverride;
1409
- return normalizePersonalityKey(input.fallback);
1410
- }
1411
- function applyCodexRuntimeDefaultsToParams(input) {
1412
- const options = input.output.options;
1413
- const modelOptions = input.modelOptions;
1414
- const defaults = readModelRuntimeDefaults(modelOptions);
1415
- const codexInstructions = asString(modelOptions.codexInstructions);
1416
- if (codexInstructions && (input.preferCodexInstructions || asString(options.instructions) === undefined)) {
1417
- options.instructions = codexInstructions;
1418
- }
1419
- if (asString(options.reasoningEffort) === undefined && defaults.defaultReasoningEffort) {
1420
- options.reasoningEffort = defaults.defaultReasoningEffort;
1421
- }
1422
- const reasoningEffort = asString(options.reasoningEffort);
1423
- const hasReasoning = reasoningEffort !== undefined && reasoningEffort !== "none";
1424
- const rawReasoningSummary = asString(options.reasoningSummary);
1425
- const hadExplicitReasoningSummary = rawReasoningSummary !== undefined;
1426
- const currentReasoningSummary = normalizeReasoningSummaryOption(rawReasoningSummary);
1427
- if (rawReasoningSummary !== undefined) {
1428
- if (currentReasoningSummary) {
1429
- options.reasoningSummary = currentReasoningSummary;
1430
- }
1431
- else {
1432
- delete options.reasoningSummary;
1433
- }
1434
- }
1435
- if (!hadExplicitReasoningSummary && currentReasoningSummary === undefined) {
1436
- if (hasReasoning &&
1437
- (defaults.supportsReasoningSummaries === true || input.thinkingSummariesOverride === true)) {
1438
- if (input.thinkingSummariesOverride === false) {
1439
- delete options.reasoningSummary;
1440
- }
1441
- else {
1442
- if (defaults.reasoningSummaryFormat?.toLowerCase() === "none") {
1443
- delete options.reasoningSummary;
1444
- }
1445
- else {
1446
- options.reasoningSummary = defaults.reasoningSummaryFormat ?? "auto";
1447
- }
1448
- }
1449
- }
1450
- }
1451
- if (asString(options.textVerbosity) === undefined &&
1452
- defaults.defaultVerbosity &&
1453
- (defaults.supportsVerbosity ?? true)) {
1454
- options.textVerbosity = defaults.defaultVerbosity;
1455
- }
1456
- if (asString(options.applyPatchToolType) === undefined && defaults.applyPatchToolType) {
1457
- options.applyPatchToolType = defaults.applyPatchToolType;
1458
- }
1459
- if (typeof options.parallelToolCalls !== "boolean" && input.modelToolCallCapable !== undefined) {
1460
- options.parallelToolCalls = input.modelToolCallCapable;
1461
- }
1462
- const shouldIncludeReasoning = hasReasoning &&
1463
- ((asString(options.reasoningSummary) !== undefined &&
1464
- asString(options.reasoningSummary)?.toLowerCase() !== "none") ||
1465
- defaults.supportsReasoningSummaries === true);
1466
- if (shouldIncludeReasoning) {
1467
- const include = asStringArray(options.include) ?? [];
1468
- options.include = mergeUnique([...include, "reasoning.encrypted_content"]);
1469
- }
1470
- }
1471
- async function sanitizeOutboundRequestIfNeeded(request, enabled) {
1472
- if (!enabled)
1473
- return { request, changed: false };
1474
- const method = request.method.toUpperCase();
1475
- if (method !== "POST")
1476
- return { request, changed: false };
1477
- let payload;
1478
- try {
1479
- const raw = await request.clone().text();
1480
- if (!raw)
1481
- return { request, changed: false };
1482
- payload = JSON.parse(raw);
1483
- }
1484
- catch {
1485
- return { request, changed: false };
1486
- }
1487
- if (!isRecord(payload))
1488
- return { request, changed: false };
1489
- const sanitized = sanitizeRequestPayloadForCompat(payload);
1490
- if (!sanitized.changed)
1491
- return { request, changed: false };
1492
- const headers = new Headers(request.headers);
1493
- headers.set("content-type", "application/json");
1494
- const sanitizedRequest = new Request(request.url, {
1495
- method: request.method,
1496
- headers,
1497
- body: JSON.stringify(sanitized.payload),
1498
- redirect: request.redirect
1499
- });
1500
- return { request: sanitizedRequest, changed: true };
1501
- }
1502
- function getVariantCandidatesFromBody(input) {
1503
- const out = [];
1504
- const seen = new Set();
1505
- const add = (value) => {
1506
- const trimmed = value?.trim().toLowerCase();
1507
- if (!trimmed)
1508
- return;
1509
- if (seen.has(trimmed))
1510
- return;
1511
- seen.add(trimmed);
1512
- out.push(trimmed);
1513
- };
1514
- const reasoning = isRecord(input.body.reasoning) ? input.body.reasoning : undefined;
1515
- add(asString(reasoning?.effort));
1516
- const normalizedSlug = input.modelSlug.trim().toLowerCase();
1517
- const suffix = normalizedSlug.match(EFFORT_SUFFIX_REGEX)?.[1];
1518
- add(suffix);
1519
- return out;
1520
- }
1521
- async function applyCatalogInstructionOverrideToRequest(input) {
1522
- if (!input.enabled)
1523
- return { request: input.request, changed: false };
1524
- const method = input.request.method.toUpperCase();
1525
- if (method !== "POST")
1526
- return { request: input.request, changed: false };
1527
- let payload;
1528
- try {
1529
- const raw = await input.request.clone().text();
1530
- if (!raw)
1531
- return { request: input.request, changed: false };
1532
- payload = JSON.parse(raw);
1533
- }
1534
- catch {
1535
- return { request: input.request, changed: false };
1536
- }
1537
- if (!isRecord(payload))
1538
- return { request: input.request, changed: false };
1539
- const modelSlugRaw = asString(payload.model);
1540
- if (!modelSlugRaw)
1541
- return { request: input.request, changed: false };
1542
- const modelCandidates = getModelLookupCandidates({
1543
- id: modelSlugRaw,
1544
- api: { id: modelSlugRaw }
1545
- });
1546
- const variantCandidates = getVariantCandidatesFromBody({
1547
- body: payload,
1548
- modelSlug: modelSlugRaw
1549
- });
1550
- const effectivePersonality = resolvePersonalityForModel({
1551
- customSettings: input.customSettings,
1552
- modelCandidates,
1553
- variantCandidates,
1554
- fallback: input.fallbackPersonality
1555
- });
1556
- const catalogModel = findCatalogModelForCandidates(input.catalogModels, modelCandidates);
1557
- if (!catalogModel)
1558
- return { request: input.request, changed: false };
1559
- const rendered = resolveInstructionsForModel(catalogModel, effectivePersonality);
1560
- if (!rendered)
1561
- return { request: input.request, changed: false };
1562
- if (asString(payload.instructions) === rendered) {
1563
- return { request: input.request, changed: false };
1564
- }
1565
- payload.instructions = rendered;
1566
- const headers = new Headers(input.request.headers);
1567
- headers.set("content-type", "application/json");
1568
- const updatedRequest = new Request(input.request.url, {
1569
- method: input.request.method,
1570
- headers,
1571
- body: JSON.stringify(payload),
1572
- redirect: input.request.redirect
1573
- });
1574
- return { request: updatedRequest, changed: true };
1575
- }
1576
652
  export async function CodexAuthPlugin(input, opts = {}) {
1577
653
  opts.log?.debug("codex-native init");
1578
- const spoofMode = opts.spoofMode === "codex" ||
1579
- opts.spoofMode === "strict"
654
+ const spoofMode = opts.spoofMode === "codex" || opts.spoofMode === "strict"
1580
655
  ? "codex"
1581
656
  : "native";
1582
657
  const runtimeMode = opts.mode === "collab" || opts.mode === "codex" || opts.mode === "native"
@@ -1586,11 +661,15 @@ export async function CodexAuthPlugin(input, opts = {}) {
1586
661
  : "native";
1587
662
  const collabModeEnabled = runtimeMode === "collab";
1588
663
  const authMode = modeForRuntimeMode(runtimeMode);
664
+ void refreshCodexClientVersionFromGitHub(opts.log).catch(() => { });
1589
665
  const resolveCatalogHeaders = () => {
1590
666
  const originator = resolveCodexOriginator(spoofMode);
667
+ const codexClientVersion = resolveCodexClientVersion();
1591
668
  return {
1592
669
  originator,
1593
670
  userAgent: resolveRequestUserAgent(spoofMode, originator),
671
+ clientVersion: codexClientVersion,
672
+ versionHeader: codexClientVersion,
1594
673
  ...(spoofMode === "native" ? { openaiBeta: "responses=experimental" } : {})
1595
674
  };
1596
675
  };
@@ -1599,6 +678,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
1599
678
  log: opts.log
1600
679
  });
1601
680
  let lastCatalogModels;
681
+ const quotaFetchCooldownByIdentity = new Map();
1602
682
  const showToast = async (message, variant = "info", quietMode = false) => {
1603
683
  if (quietMode)
1604
684
  return;
@@ -1616,6 +696,8 @@ export async function CodexAuthPlugin(input, opts = {}) {
1616
696
  };
1617
697
  const refreshQuotaSnapshotsForAuthMenu = async () => {
1618
698
  const auth = await loadAuthStorage();
699
+ const snapshotPath = defaultSnapshotsPath();
700
+ const existingSnapshots = await loadSnapshots(snapshotPath).catch(() => ({}));
1619
701
  const snapshotUpdates = {};
1620
702
  for (const { mode, domain } of listOpenAIOAuthDomains(auth)) {
1621
703
  for (let index = 0; index < domain.accounts.length; index += 1) {
@@ -1623,11 +705,22 @@ export async function CodexAuthPlugin(input, opts = {}) {
1623
705
  if (!account || account.enabled === false)
1624
706
  continue;
1625
707
  hydrateAccountIdentityFromAccessClaims(account);
1626
- let accessToken = typeof account.access === "string" && account.access.length > 0 ? account.access : undefined;
708
+ const identityKey = account.identityKey;
1627
709
  const now = Date.now();
1628
- const expired = typeof account.expires === "number" &&
1629
- Number.isFinite(account.expires) &&
1630
- account.expires <= now;
710
+ if (identityKey) {
711
+ const cooldownUntil = quotaFetchCooldownByIdentity.get(identityKey);
712
+ if (typeof cooldownUntil === "number" && cooldownUntil > now)
713
+ continue;
714
+ const existing = existingSnapshots[identityKey];
715
+ if (existing &&
716
+ typeof existing.updatedAt === "number" &&
717
+ Number.isFinite(existing.updatedAt) &&
718
+ now - existing.updatedAt < AUTH_MENU_QUOTA_SNAPSHOT_TTL_MS) {
719
+ continue;
720
+ }
721
+ }
722
+ let accessToken = typeof account.access === "string" && account.access.length > 0 ? account.access : undefined;
723
+ const expired = typeof account.expires === "number" && Number.isFinite(account.expires) && account.expires <= now;
1631
724
  if ((!accessToken || expired) && account.refresh) {
1632
725
  try {
1633
726
  await saveAuthStorage(undefined, async (authFile) => {
@@ -1658,6 +751,9 @@ export async function CodexAuthPlugin(input, opts = {}) {
1658
751
  });
1659
752
  }
1660
753
  catch (error) {
754
+ if (identityKey) {
755
+ quotaFetchCooldownByIdentity.set(identityKey, Date.now() + AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS);
756
+ }
1661
757
  opts.log?.debug("quota check refresh failed", {
1662
758
  index,
1663
759
  mode,
@@ -1678,16 +774,20 @@ export async function CodexAuthPlugin(input, opts = {}) {
1678
774
  now: Date.now(),
1679
775
  modelFamily: "gpt-5.3-codex",
1680
776
  userAgent: resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode)),
1681
- log: opts.log
777
+ log: opts.log,
778
+ timeoutMs: AUTH_MENU_QUOTA_FETCH_TIMEOUT_MS
1682
779
  });
1683
- if (!snapshot)
780
+ if (!snapshot) {
781
+ quotaFetchCooldownByIdentity.set(account.identityKey, Date.now() + AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS);
1684
782
  continue;
783
+ }
784
+ quotaFetchCooldownByIdentity.delete(account.identityKey);
1685
785
  snapshotUpdates[account.identityKey] = snapshot;
1686
786
  }
1687
787
  }
1688
788
  if (Object.keys(snapshotUpdates).length === 0)
1689
789
  return;
1690
- await saveSnapshots(defaultSnapshotsPath(), (current) => ({
790
+ await saveSnapshots(snapshotPath, (current) => ({
1691
791
  ...current,
1692
792
  ...snapshotUpdates
1693
793
  }));
@@ -1779,9 +879,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
1779
879
  },
1780
880
  onToggleAccount: async (account) => {
1781
881
  await saveAuthStorage(undefined, (authFile) => {
1782
- const authTypes = account.authTypes && account.authTypes.length > 0
1783
- ? [...account.authTypes]
1784
- : ["native"];
882
+ const authTypes = account.authTypes && account.authTypes.length > 0 ? [...account.authTypes] : ["native"];
1785
883
  for (const mode of authTypes) {
1786
884
  const domain = getOpenAIOAuthDomain(authFile, mode);
1787
885
  if (!domain)
@@ -1893,7 +991,9 @@ export async function CodexAuthPlugin(input, opts = {}) {
1893
991
  if (!hasOAuth)
1894
992
  return {};
1895
993
  const sessionAffinityPath = defaultSessionAffinityPath();
1896
- const loadedSessionAffinity = await loadSessionAffinity(sessionAffinityPath).catch(() => ({ version: 1 }));
994
+ const loadedSessionAffinity = await loadSessionAffinity(sessionAffinityPath).catch(() => ({
995
+ version: 1
996
+ }));
1897
997
  const initialSessionAffinity = readSessionAffinitySnapshot(loadedSessionAffinity, authMode);
1898
998
  const sessionExists = createSessionExistsFn(process.env);
1899
999
  await pruneSessionAffinitySnapshot(initialSessionAffinity, sessionExists, {
@@ -1933,13 +1033,30 @@ export async function CodexAuthPlugin(input, opts = {}) {
1933
1033
  ...resolveCatalogHeaders(),
1934
1034
  onEvent: (event) => opts.log?.debug("codex model catalog", event)
1935
1035
  });
1936
- lastCatalogModels = catalogModels;
1937
- applyCodexCatalogToProviderModels({
1938
- providerModels: provider.models,
1939
- catalogModels,
1940
- fallbackModels: STATIC_FALLBACK_MODELS,
1941
- personality: opts.personality
1942
- });
1036
+ const applyCatalogModels = (models) => {
1037
+ if (models) {
1038
+ lastCatalogModels = models;
1039
+ }
1040
+ applyCodexCatalogToProviderModels({
1041
+ providerModels: provider.models,
1042
+ catalogModels: models ?? lastCatalogModels,
1043
+ fallbackModels: STATIC_FALLBACK_MODELS,
1044
+ personality: opts.personality
1045
+ });
1046
+ };
1047
+ applyCatalogModels(catalogModels);
1048
+ const syncCatalogFromAuth = async (auth) => {
1049
+ if (!auth.accessToken)
1050
+ return undefined;
1051
+ const refreshedCatalog = await getCodexModelCatalog({
1052
+ accessToken: auth.accessToken,
1053
+ accountId: auth.accountId,
1054
+ ...resolveCatalogHeaders(),
1055
+ onEvent: (event) => opts.log?.debug("codex model catalog", event)
1056
+ });
1057
+ applyCatalogModels(refreshedCatalog);
1058
+ return refreshedCatalog;
1059
+ };
1943
1060
  return {
1944
1061
  apiKey: OAUTH_DUMMY_KEY,
1945
1062
  async fetch(requestInput, init) {
@@ -1948,9 +1065,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
1948
1065
  if (opts.headerTransformDebug === true) {
1949
1066
  await requestSnapshots.captureRequest("before-header-transform", baseRequest, {
1950
1067
  spoofMode,
1951
- ...(inboundCollaborationModeKind
1952
- ? { collaborationModeKind: inboundCollaborationModeKind }
1953
- : {})
1068
+ ...(inboundCollaborationModeKind ? { collaborationModeKind: inboundCollaborationModeKind } : {})
1954
1069
  });
1955
1070
  }
1956
1071
  let outbound = new Request(rewriteUrl(baseRequest), baseRequest);
@@ -1986,6 +1101,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
1986
1101
  await requestSnapshots.captureRequest("after-header-transform", outbound, {
1987
1102
  spoofMode,
1988
1103
  instructionsOverridden: instructionOverride.changed,
1104
+ instructionOverrideReason: instructionOverride.reason,
1989
1105
  ...(collaborationModeKind ? { collaborationModeKind } : {}),
1990
1106
  ...(isSubagentRequest ? { subagent: subagentHeader } : {})
1991
1107
  });
@@ -2122,8 +1238,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2122
1238
  tokens = await refreshAccessToken(selected.refresh);
2123
1239
  }
2124
1240
  catch (error) {
2125
- if (isOAuthTokenRefreshError(error) &&
2126
- error.oauthCode?.toLowerCase() === "invalid_grant") {
1241
+ if (isOAuthTokenRefreshError(error) && error.oauthCode?.toLowerCase() === "invalid_grant") {
2127
1242
  sawInvalidGrant = true;
2128
1243
  selected.enabled = false;
2129
1244
  delete selected.cooldownUntil;
@@ -2233,12 +1348,23 @@ export async function CodexAuthPlugin(input, opts = {}) {
2233
1348
  param: "auth"
2234
1349
  });
2235
1350
  }
2236
- void getCodexModelCatalog({
2237
- accessToken: access,
2238
- accountId,
2239
- ...resolveCatalogHeaders(),
2240
- onEvent: (event) => opts.log?.debug("codex model catalog", event)
2241
- }).catch(() => { });
1351
+ if (spoofMode === "codex") {
1352
+ const shouldAwaitCatalog = !lastCatalogModels || lastCatalogModels.length === 0;
1353
+ if (shouldAwaitCatalog) {
1354
+ try {
1355
+ await syncCatalogFromAuth({ accessToken: access, accountId });
1356
+ }
1357
+ catch {
1358
+ // best-effort catalog load; request can still proceed
1359
+ }
1360
+ }
1361
+ else {
1362
+ void syncCatalogFromAuth({ accessToken: access, accountId }).catch(() => { });
1363
+ }
1364
+ }
1365
+ else {
1366
+ void syncCatalogFromAuth({ accessToken: access, accountId }).catch(() => { });
1367
+ }
2242
1368
  selectedIdentityKey = identityKey;
2243
1369
  return { access, accountId, identityKey, accountLabel, email, plan };
2244
1370
  },
@@ -2260,14 +1386,24 @@ export async function CodexAuthPlugin(input, opts = {}) {
2260
1386
  },
2261
1387
  showToast,
2262
1388
  onAttemptRequest: async ({ attempt, maxAttempts, request, auth, sessionKey }) => {
2263
- await requestSnapshots.captureRequest("outbound-attempt", request, {
1389
+ const instructionOverride = await applyCatalogInstructionOverrideToRequest({
1390
+ request,
1391
+ enabled: spoofMode === "codex",
1392
+ catalogModels: lastCatalogModels,
1393
+ customSettings: opts.customSettings,
1394
+ fallbackPersonality: opts.personality
1395
+ });
1396
+ await requestSnapshots.captureRequest("outbound-attempt", instructionOverride.request, {
2264
1397
  attempt: attempt + 1,
2265
1398
  maxAttempts,
2266
1399
  sessionKey,
2267
1400
  identityKey: auth.identityKey,
2268
1401
  accountLabel: auth.accountLabel,
1402
+ instructionsOverridden: instructionOverride.changed,
1403
+ instructionOverrideReason: instructionOverride.reason,
2269
1404
  ...(collaborationModeKind ? { collaborationModeKind } : {})
2270
1405
  });
1406
+ return instructionOverride.request;
2271
1407
  },
2272
1408
  onAttemptResponse: async ({ attempt, maxAttempts, response, auth, sessionKey }) => {
2273
1409
  await requestSnapshots.captureResponse("outbound-response", response, {
@@ -2289,6 +1425,20 @@ export async function CodexAuthPlugin(input, opts = {}) {
2289
1425
  sanitized: sanitizedOutbound.changed,
2290
1426
  ...(collaborationModeKind ? { collaborationModeKind } : {})
2291
1427
  });
1428
+ try {
1429
+ assertAllowedOutboundUrl(new URL(sanitizedOutbound.request.url));
1430
+ }
1431
+ catch (error) {
1432
+ if (isPluginFatalError(error)) {
1433
+ return toSyntheticErrorResponse(error);
1434
+ }
1435
+ return toSyntheticErrorResponse(new PluginFatalError({
1436
+ message: "Outbound request validation failed before sending to OpenAI backend.",
1437
+ status: 400,
1438
+ type: "disallowed_outbound_request",
1439
+ param: "request"
1440
+ }));
1441
+ }
2292
1442
  let response;
2293
1443
  try {
2294
1444
  response = await orchestrator.execute(sanitizedOutbound.request);
@@ -2368,7 +1518,6 @@ export async function CodexAuthPlugin(input, opts = {}) {
2368
1518
  return null;
2369
1519
  }
2370
1520
  finally {
2371
- pendingOAuth = undefined;
2372
1521
  scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
2373
1522
  }
2374
1523
  };
@@ -2406,10 +1555,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2406
1555
  };
2407
1556
  }
2408
1557
  };
2409
- if (inputs &&
2410
- process.env.OPENCODE_NO_BROWSER !== "1" &&
2411
- process.stdin.isTTY &&
2412
- process.stdout.isTTY) {
1558
+ if (inputs && process.env.OPENCODE_NO_BROWSER !== "1" && process.stdin.isTTY && process.stdout.isTTY) {
2413
1559
  return runInteractiveBrowserAuthLoop();
2414
1560
  }
2415
1561
  const { redirectUri } = await startOAuthServer();
@@ -2434,7 +1580,6 @@ export async function CodexAuthPlugin(input, opts = {}) {
2434
1580
  return { type: "failed" };
2435
1581
  }
2436
1582
  finally {
2437
- pendingOAuth = undefined;
2438
1583
  scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
2439
1584
  }
2440
1585
  }
@@ -2518,9 +1663,8 @@ export async function CodexAuthPlugin(input, opts = {}) {
2518
1663
  },
2519
1664
  "chat.message": async (hookInput, output) => {
2520
1665
  const directProviderID = hookInput.model?.providerID;
2521
- const isOpenAI = directProviderID === "openai"
2522
- || (directProviderID === undefined
2523
- && (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)));
1666
+ const isOpenAI = directProviderID === "openai" ||
1667
+ (directProviderID === undefined && (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)));
2524
1668
  if (!isOpenAI)
2525
1669
  return;
2526
1670
  for (const part of output.parts) {
@@ -2536,9 +1680,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2536
1680
  if (hookInput.model.providerID !== "openai")
2537
1681
  return;
2538
1682
  const initialReasoningEffort = asString(output.options.reasoningEffort);
2539
- const collaborationProfile = collabModeEnabled
2540
- ? resolveCollaborationProfile(hookInput.agent)
2541
- : { enabled: false };
1683
+ const collaborationProfile = collabModeEnabled ? resolveCollaborationProfile(hookInput.agent) : { enabled: false };
2542
1684
  const modelOptions = isRecord(hookInput.model.options) ? hookInput.model.options : {};
2543
1685
  const modelCandidates = getModelLookupCandidates({
2544
1686
  id: hookInput.model.id,
@@ -2594,7 +1736,12 @@ export async function CodexAuthPlugin(input, opts = {}) {
2594
1736
  });
2595
1737
  if (collabModeEnabled && collaborationProfile.enabled && collaborationProfile.kind) {
2596
1738
  const collaborationModeKind = collaborationProfile.kind;
2597
- const collaborationInstructions = resolveCollaborationInstructions(collaborationModeKind);
1739
+ const collaborationInstructions = resolveCollaborationInstructions(collaborationModeKind, {
1740
+ plan: CODEX_PLAN_MODE_INSTRUCTIONS,
1741
+ code: CODEX_CODE_MODE_INSTRUCTIONS,
1742
+ execute: CODEX_EXECUTE_MODE_INSTRUCTIONS,
1743
+ pairProgramming: CODEX_PAIR_PROGRAMMING_MODE_INSTRUCTIONS
1744
+ });
2598
1745
  const mergedInstructions = mergeInstructions(asString(output.options.instructions), collaborationInstructions);
2599
1746
  if (mergedInstructions) {
2600
1747
  output.options.instructions = mergedInstructions;
@@ -2612,9 +1759,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2612
1759
  "chat.headers": async (hookInput, output) => {
2613
1760
  if (hookInput.model.providerID !== "openai")
2614
1761
  return;
2615
- const collaborationProfile = collabModeEnabled
2616
- ? resolveCollaborationProfile(hookInput.agent)
2617
- : { enabled: false };
1762
+ const collaborationProfile = collabModeEnabled ? resolveCollaborationProfile(hookInput.agent) : { enabled: false };
2618
1763
  const collaborationModeKind = collaborationProfile.enabled ? collaborationProfile.kind : undefined;
2619
1764
  const originator = resolveCodexOriginator(spoofMode);
2620
1765
  output.headers.originator = originator;