@iam-brain/opencode-codex-auth 0.1.16 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) 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 +145 -1035
  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.d.ts +5 -1
  41. package/dist/lib/codex-status-tool.d.ts.map +1 -1
  42. package/dist/lib/codex-status-tool.js +8 -6
  43. package/dist/lib/codex-status-tool.js.map +1 -1
  44. package/dist/lib/codex-status-ui.d.ts +4 -0
  45. package/dist/lib/codex-status-ui.d.ts.map +1 -1
  46. package/dist/lib/codex-status-ui.js +91 -28
  47. package/dist/lib/codex-status-ui.js.map +1 -1
  48. package/dist/lib/compat-sanitizer.d.ts.map +1 -1
  49. package/dist/lib/compat-sanitizer.js +2 -1
  50. package/dist/lib/compat-sanitizer.js.map +1 -1
  51. package/dist/lib/config.d.ts.map +1 -1
  52. package/dist/lib/config.js +28 -70
  53. package/dist/lib/config.js.map +1 -1
  54. package/dist/lib/fetch-orchestrator.d.ts.map +1 -1
  55. package/dist/lib/fetch-orchestrator.js +2 -9
  56. package/dist/lib/fetch-orchestrator.js.map +1 -1
  57. package/dist/lib/identity.d.ts.map +1 -1
  58. package/dist/lib/identity.js.map +1 -1
  59. package/dist/lib/installer-cli.d.ts.map +1 -1
  60. package/dist/lib/installer-cli.js +1 -3
  61. package/dist/lib/installer-cli.js.map +1 -1
  62. package/dist/lib/logger.d.ts.map +1 -1
  63. package/dist/lib/logger.js +13 -5
  64. package/dist/lib/logger.js.map +1 -1
  65. package/dist/lib/model-catalog.d.ts +2 -0
  66. package/dist/lib/model-catalog.d.ts.map +1 -1
  67. package/dist/lib/model-catalog.js +117 -41
  68. package/dist/lib/model-catalog.js.map +1 -1
  69. package/dist/lib/orchestrator-agents.d.ts.map +1 -1
  70. package/dist/lib/orchestrator-agents.js.map +1 -1
  71. package/dist/lib/paths.d.ts.map +1 -1
  72. package/dist/lib/paths.js.map +1 -1
  73. package/dist/lib/persona-tool-cli.d.ts.map +1 -1
  74. package/dist/lib/persona-tool-cli.js.map +1 -1
  75. package/dist/lib/persona-tool.d.ts.map +1 -1
  76. package/dist/lib/persona-tool.js +10 -31
  77. package/dist/lib/persona-tool.js.map +1 -1
  78. package/dist/lib/personalities.d.ts.map +1 -1
  79. package/dist/lib/personalities.js.map +1 -1
  80. package/dist/lib/proactive-refresh.d.ts.map +1 -1
  81. package/dist/lib/proactive-refresh.js +3 -6
  82. package/dist/lib/proactive-refresh.js.map +1 -1
  83. package/dist/lib/refresh-queue.d.ts.map +1 -1
  84. package/dist/lib/refresh-queue.js +4 -2
  85. package/dist/lib/refresh-queue.js.map +1 -1
  86. package/dist/lib/request-snapshots.d.ts.map +1 -1
  87. package/dist/lib/request-snapshots.js.map +1 -1
  88. package/dist/lib/rotation.d.ts.map +1 -1
  89. package/dist/lib/rotation.js +2 -5
  90. package/dist/lib/rotation.js.map +1 -1
  91. package/dist/lib/session-affinity.d.ts.map +1 -1
  92. package/dist/lib/session-affinity.js.map +1 -1
  93. package/dist/lib/storage.d.ts.map +1 -1
  94. package/dist/lib/storage.js +2 -6
  95. package/dist/lib/storage.js.map +1 -1
  96. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  97. package/dist/lib/ui/auth-menu.js +1 -3
  98. package/dist/lib/ui/auth-menu.js.map +1 -1
  99. package/dist/lib/ui/tty/ansi.d.ts.map +1 -1
  100. package/dist/lib/ui/tty/ansi.js.map +1 -1
  101. package/dist/lib/ui/tty/confirm.d.ts.map +1 -1
  102. package/dist/lib/ui/tty/confirm.js.map +1 -1
  103. package/dist/lib/ui/tty/select.d.ts.map +1 -1
  104. package/dist/lib/ui/tty/select.js +1 -3
  105. package/dist/lib/ui/tty/select.js.map +1 -1
  106. 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";
@@ -16,13 +10,21 @@ import { toolOutputForStatus } from "./codex-status-tool";
16
10
  import { FetchOrchestrator, createFetchOrchestratorState } from "./fetch-orchestrator";
17
11
  import { formatToastMessage } from "./toast";
18
12
  import { runAuthMenuOnce } from "./ui/auth-menu-runner";
13
+ import { shouldUseColor } from "./ui/tty/ansi";
19
14
  import { applyCodexCatalogToProviderModels, getCodexModelCatalog, getRuntimeDefaultsForModel, resolveInstructionsForModel } from "./model-catalog";
20
15
  import { CODEX_RS_COMPACT_PROMPT } from "./orchestrator-agents";
21
- import { sanitizeRequestPayloadForCompat } from "./compat-sanitizer";
22
16
  import { fetchQuotaSnapshotFromBackend } from "./codex-quota-fetch";
23
17
  import { createRequestSnapshots } from "./request-snapshots";
24
18
  import { CODEX_OAUTH_SUCCESS_HTML } from "./oauth-pages";
19
+ import { mergeInstructions, resolveCollaborationInstructions, resolveCollaborationModeKind, resolveCollaborationProfile, resolveHookAgentName, resolveSubagentHeaderValue } from "./codex-native/collaboration";
20
+ import { applyCatalogInstructionOverrideToRequest, applyCodexRuntimeDefaultsToParams, findCatalogModelForCandidates, getModelLookupCandidates, getModelThinkingSummariesOverride, getVariantLookupCandidates, resolvePersonalityForModel, sanitizeOutboundRequestIfNeeded } from "./codex-native/request-transform";
25
21
  import { createSessionExistsFn, loadSessionAffinity, pruneSessionAffinitySnapshot, readSessionAffinitySnapshot, saveSessionAffinity, writeSessionAffinitySnapshot } from "./session-affinity";
22
+ import { resolveCodexOriginator } from "./codex-native/originator";
23
+ import { tryOpenUrlInBrowser as openUrlInBrowser } from "./codex-native/browser";
24
+ import { selectCatalogAuthCandidate } from "./codex-native/catalog-auth";
25
+ import { buildCodexUserAgent, refreshCodexClientVersionFromGitHub, resolveCodexClientVersion, resolveRequestUserAgent } from "./codex-native/client-identity";
26
+ import { createOAuthServerController } from "./codex-native/oauth-server";
27
+ export { browserOpenInvocationFor } from "./codex-native/browser";
26
28
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
27
29
  const ISSUER = "https://auth.openai.com";
28
30
  const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
@@ -55,10 +57,10 @@ const OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS = (() => {
55
57
  const parsed = Number(raw);
56
58
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
57
59
  })();
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";
60
+ const OPENAI_OUTBOUND_HOST_ALLOWLIST = new Set(["api.openai.com", "auth.openai.com", "chat.openai.com", "chatgpt.com"]);
61
+ const AUTH_MENU_QUOTA_SNAPSHOT_TTL_MS = 60_000;
62
+ const AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS = 30_000;
63
+ const AUTH_MENU_QUOTA_FETCH_TIMEOUT_MS = 5000;
62
64
  const INTERNAL_COLLABORATION_MODE_HEADER = "x-opencode-collaboration-mode-kind";
63
65
  const SESSION_AFFINITY_MISSING_GRACE_MS = 15 * 60 * 1000;
64
66
  const CODEX_PLAN_MODE_INSTRUCTIONS = [
@@ -93,38 +95,12 @@ const STATIC_FALLBACK_MODELS = [
93
95
  function sleep(ms) {
94
96
  return new Promise((resolve) => setTimeout(resolve, ms));
95
97
  }
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
98
  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
- }
99
+ return openUrlInBrowser({
100
+ url,
101
+ log,
102
+ onEvent: (event, meta) => oauthServerController.emitDebug(event, meta ?? {})
103
+ });
128
104
  }
129
105
  function escapeHtml(value) {
130
106
  return value
@@ -141,11 +117,7 @@ async function generatePKCE() {
141
117
  return { verifier, challenge };
142
118
  }
143
119
  function base64UrlEncode(bytes) {
144
- return Buffer.from(bytes)
145
- .toString("base64")
146
- .replace(/\+/g, "-")
147
- .replace(/\//g, "_")
148
- .replace(/=+$/g, "");
120
+ return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
149
121
  }
150
122
  function generateState() {
151
123
  return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
@@ -195,6 +167,8 @@ export const __testOnly = {
195
167
  modeForRuntimeMode,
196
168
  buildCodexUserAgent,
197
169
  resolveRequestUserAgent,
170
+ resolveCodexClientVersion,
171
+ refreshCodexClientVersionFromGitHub,
198
172
  resolveHookAgentName,
199
173
  resolveCollaborationModeKind,
200
174
  resolveSubagentHeaderValue,
@@ -278,8 +252,7 @@ function composeCodexSuccessRedirectUrl(tokens, options = {}) {
278
252
  const port = options.port ?? OAUTH_PORT;
279
253
  const idClaims = getOpenAIAuthClaims(tokens.id_token);
280
254
  const accessClaims = getOpenAIAuthClaims(tokens.access_token);
281
- const needsSetup = !getClaimBoolean(idClaims, "completed_platform_onboarding") &&
282
- getClaimBoolean(idClaims, "is_org_owner");
255
+ const needsSetup = !getClaimBoolean(idClaims, "completed_platform_onboarding") && getClaimBoolean(idClaims, "is_org_owner");
283
256
  const platformUrl = issuer === ISSUER ? "https://platform.openai.com" : "https://platform.api.openai.org";
284
257
  const params = new URLSearchParams({
285
258
  needs_setup: String(needsSetup),
@@ -375,254 +348,32 @@ function buildOAuthErrorHtml(error) {
375
348
  </body>
376
349
  </html>`;
377
350
  }
378
- let oauthServer;
379
- let pendingOAuth;
380
- let oauthServerCloseTimer;
351
+ const oauthServerController = createOAuthServerController({
352
+ port: OAUTH_PORT,
353
+ loopbackHost: OAUTH_LOOPBACK_HOST,
354
+ callbackOrigin: OAUTH_CALLBACK_ORIGIN,
355
+ callbackUri: OAUTH_CALLBACK_URI,
356
+ callbackPath: OAUTH_CALLBACK_PATH,
357
+ callbackTimeoutMs: OAUTH_CALLBACK_TIMEOUT_MS,
358
+ buildOAuthErrorHtml,
359
+ buildOAuthSuccessHtml,
360
+ composeCodexSuccessRedirectUrl,
361
+ exchangeCodeForTokens
362
+ });
381
363
  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
- }
364
+ return oauthServerController.isDebugEnabled();
433
365
  }
434
366
  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 };
367
+ return oauthServerController.start();
569
368
  }
570
369
  function stopOAuthServer() {
571
- clearOAuthServerCloseTimer();
572
- emitOAuthDebug("server_stopping", { hadPendingOAuth: Boolean(pendingOAuth) });
573
- oauthServer?.close();
574
- oauthServer = undefined;
575
- emitOAuthDebug("server_stopped");
370
+ oauthServerController.stop();
576
371
  }
577
372
  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);
373
+ oauthServerController.scheduleStop(delayMs, reason);
589
374
  }
590
375
  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
- });
376
+ return oauthServerController.waitForCallback(pkce, state, authMode);
626
377
  }
627
378
  function modeForRuntimeMode(runtimeMode) {
628
379
  return runtimeMode === "native" ? "native" : "codex";
@@ -711,319 +462,38 @@ function rewriteUrl(requestInput) {
711
462
  const parsed = requestInput instanceof URL
712
463
  ? requestInput
713
464
  : new URL(typeof requestInput === "string" ? requestInput : requestInput.url);
714
- if (parsed.pathname.includes("/v1/responses") ||
715
- parsed.pathname.includes("/chat/completions")) {
465
+ if (parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions")) {
716
466
  return new URL(CODEX_API_ENDPOINT);
717
467
  }
718
468
  return parsed;
719
469
  }
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")
470
+ function isAllowedOpenAIOutboundHost(hostname) {
471
+ const normalized = hostname.trim().toLowerCase();
472
+ if (!normalized)
934
473
  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";
474
+ if (OPENAI_OUTBOUND_HOST_ALLOWLIST.has(normalized))
475
+ return true;
476
+ return normalized.endsWith(".openai.com") || normalized.endsWith(".chatgpt.com");
477
+ }
478
+ function assertAllowedOutboundUrl(url) {
479
+ const protocol = url.protocol.trim().toLowerCase();
480
+ if (protocol !== "https:") {
481
+ throw new PluginFatalError({
482
+ message: `Blocked outbound request with unsupported protocol "${protocol || "unknown"}". ` +
483
+ "This plugin only proxies HTTPS requests to OpenAI/ChatGPT backends.",
484
+ status: 400,
485
+ type: "disallowed_outbound_protocol",
486
+ param: "request"
487
+ });
1021
488
  }
1022
- if (normalized.includes("review"))
1023
- return "review";
1024
- if (normalized.includes("compact") || normalized.includes("compaction"))
1025
- return "compact";
1026
- return "collab_spawn";
489
+ if (isAllowedOpenAIOutboundHost(url.hostname))
490
+ return;
491
+ throw new PluginFatalError({
492
+ message: `Blocked outbound request to "${url.hostname}". ` + "This plugin only proxies OpenAI/ChatGPT backend traffic.",
493
+ status: 400,
494
+ type: "disallowed_outbound_host",
495
+ param: "request"
496
+ });
1027
497
  }
1028
498
  async function sessionUsesOpenAIProvider(client, sessionID) {
1029
499
  const sessionApi = client?.session;
@@ -1040,9 +510,7 @@ async function sessionUsesOpenAIProvider(client, sessionID) {
1040
510
  if (asString(info.role) !== "user")
1041
511
  continue;
1042
512
  const model = isRecord(info.model) ? info.model : undefined;
1043
- const providerID = model
1044
- ? asString(model.providerID)
1045
- : asString(info.providerID);
513
+ const providerID = model ? asString(model.providerID) : asString(info.providerID);
1046
514
  if (!providerID)
1047
515
  continue;
1048
516
  return providerID === "openai";
@@ -1053,26 +521,11 @@ async function sessionUsesOpenAIProvider(client, sessionID) {
1053
521
  }
1054
522
  return false;
1055
523
  }
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
524
  function formatAccountLabel(account, index) {
1068
525
  const email = account?.email?.trim();
1069
526
  const plan = account?.plan?.trim();
1070
527
  const accountId = account?.accountId?.trim();
1071
- const idSuffix = accountId
1072
- ? accountId.length > 6
1073
- ? accountId.slice(-6)
1074
- : accountId
1075
- : null;
528
+ const idSuffix = accountId ? (accountId.length > 6 ? accountId.slice(-6) : accountId) : null;
1076
529
  if (email && plan)
1077
530
  return `${email} (${plan})`;
1078
531
  if (email)
@@ -1082,7 +535,7 @@ function formatAccountLabel(account, index) {
1082
535
  return `Account ${index + 1}`;
1083
536
  }
1084
537
  function hasActiveCooldown(account, now) {
1085
- return typeof account.cooldownUntil === "number" && Number.isFinite(account.cooldownUntil) && account.cooldownUntil > now;
538
+ return (typeof account.cooldownUntil === "number" && Number.isFinite(account.cooldownUntil) && account.cooldownUntil > now);
1086
539
  }
1087
540
  function ensureAccountAuthTypes(account) {
1088
541
  const normalized = normalizeAccountAuthTypes(account.authTypes);
@@ -1128,13 +581,12 @@ function buildAuthMenuAccounts(input) {
1128
581
  const existing = rows.get(identity);
1129
582
  const currentStatus = hasActiveCooldown(account, now)
1130
583
  ? "rate-limited"
1131
- : typeof account.expires === "number" &&
1132
- Number.isFinite(account.expires) &&
1133
- account.expires <= now
584
+ : typeof account.expires === "number" && Number.isFinite(account.expires) && account.expires <= now
1134
585
  ? "expired"
1135
586
  : "unknown";
1136
587
  if (!existing) {
1137
- const isCurrentAccount = authMode === input.activeMode && Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
588
+ const isCurrentAccount = authMode === input.activeMode &&
589
+ Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
1138
590
  rows.set(identity, {
1139
591
  identityKey: account.identityKey,
1140
592
  index: rows.size,
@@ -1150,8 +602,7 @@ function buildAuthMenuAccounts(input) {
1150
602
  continue;
1151
603
  }
1152
604
  existing.authTypes = normalizeAccountAuthTypes([...(existing.authTypes ?? []), authMode]);
1153
- if (typeof account.lastUsed === "number" &&
1154
- (!existing.lastUsed || account.lastUsed > existing.lastUsed)) {
605
+ if (typeof account.lastUsed === "number" && (!existing.lastUsed || account.lastUsed > existing.lastUsed)) {
1155
606
  existing.lastUsed = account.lastUsed;
1156
607
  }
1157
608
  if (existing.enabled === false && account.enabled !== false) {
@@ -1160,12 +611,11 @@ function buildAuthMenuAccounts(input) {
1160
611
  if (existing.status !== "rate-limited" && currentStatus === "rate-limited") {
1161
612
  existing.status = "rate-limited";
1162
613
  }
1163
- else if (existing.status !== "rate-limited" &&
1164
- existing.status !== "expired" &&
1165
- currentStatus === "expired") {
614
+ else if (existing.status !== "rate-limited" && existing.status !== "expired" && currentStatus === "expired") {
1166
615
  existing.status = "expired";
1167
616
  }
1168
- const isCurrentAccount = authMode === input.activeMode && Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
617
+ const isCurrentAccount = authMode === input.activeMode &&
618
+ Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
1169
619
  if (isCurrentAccount) {
1170
620
  existing.isCurrentAccount = true;
1171
621
  existing.status = "active";
@@ -1177,9 +627,7 @@ function buildAuthMenuAccounts(input) {
1177
627
  return Array.from(rows.values()).map((row, index) => ({ ...row, index }));
1178
628
  }
1179
629
  function hydrateAccountIdentityFromAccessClaims(account) {
1180
- const claims = typeof account.access === "string" && account.access.length > 0
1181
- ? parseJwtClaims(account.access)
1182
- : undefined;
630
+ const claims = typeof account.access === "string" && account.access.length > 0 ? parseJwtClaims(account.access) : undefined;
1183
631
  if (!account.accountId)
1184
632
  account.accountId = extractAccountIdFromClaims(claims);
1185
633
  if (!account.email)
@@ -1193,35 +641,6 @@ function hydrateAccountIdentityFromAccessClaims(account) {
1193
641
  ensureAccountAuthTypes(account);
1194
642
  synchronizeIdentityKey(account);
1195
643
  }
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
644
  function isRecord(value) {
1226
645
  return typeof value === "object" && value !== null && !Array.isArray(value);
1227
646
  }
@@ -1231,352 +650,9 @@ function asString(value) {
1231
650
  const trimmed = value.trim();
1232
651
  return trimmed ? trimmed : undefined;
1233
652
  }
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, reason: "disabled" };
1524
- const method = input.request.method.toUpperCase();
1525
- if (method !== "POST")
1526
- return { request: input.request, changed: false, reason: "non_post" };
1527
- let payload;
1528
- try {
1529
- const raw = await input.request.clone().text();
1530
- if (!raw)
1531
- return { request: input.request, changed: false, reason: "empty_body" };
1532
- payload = JSON.parse(raw);
1533
- }
1534
- catch {
1535
- return { request: input.request, changed: false, reason: "invalid_json" };
1536
- }
1537
- if (!isRecord(payload))
1538
- return { request: input.request, changed: false, reason: "non_object_body" };
1539
- const modelSlugRaw = asString(payload.model);
1540
- if (!modelSlugRaw)
1541
- return { request: input.request, changed: false, reason: "missing_model" };
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, reason: "catalog_model_not_found" };
1559
- const rendered = resolveInstructionsForModel(catalogModel, effectivePersonality);
1560
- if (!rendered)
1561
- return { request: input.request, changed: false, reason: "rendered_empty_or_unsafe" };
1562
- if (asString(payload.instructions) === rendered) {
1563
- return { request: input.request, changed: false, reason: "already_matches" };
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, reason: "updated" };
1575
- }
1576
653
  export async function CodexAuthPlugin(input, opts = {}) {
1577
654
  opts.log?.debug("codex-native init");
1578
- const spoofMode = opts.spoofMode === "codex" ||
1579
- opts.spoofMode === "strict"
655
+ const spoofMode = opts.spoofMode === "codex" || opts.spoofMode === "strict"
1580
656
  ? "codex"
1581
657
  : "native";
1582
658
  const runtimeMode = opts.mode === "collab" || opts.mode === "codex" || opts.mode === "native"
@@ -1586,11 +662,15 @@ export async function CodexAuthPlugin(input, opts = {}) {
1586
662
  : "native";
1587
663
  const collabModeEnabled = runtimeMode === "collab";
1588
664
  const authMode = modeForRuntimeMode(runtimeMode);
665
+ void refreshCodexClientVersionFromGitHub(opts.log).catch(() => { });
1589
666
  const resolveCatalogHeaders = () => {
1590
667
  const originator = resolveCodexOriginator(spoofMode);
668
+ const codexClientVersion = resolveCodexClientVersion();
1591
669
  return {
1592
670
  originator,
1593
671
  userAgent: resolveRequestUserAgent(spoofMode, originator),
672
+ clientVersion: codexClientVersion,
673
+ versionHeader: codexClientVersion,
1594
674
  ...(spoofMode === "native" ? { openaiBeta: "responses=experimental" } : {})
1595
675
  };
1596
676
  };
@@ -1599,6 +679,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
1599
679
  log: opts.log
1600
680
  });
1601
681
  let lastCatalogModels;
682
+ const quotaFetchCooldownByIdentity = new Map();
1602
683
  const showToast = async (message, variant = "info", quietMode = false) => {
1603
684
  if (quietMode)
1604
685
  return;
@@ -1616,6 +697,8 @@ export async function CodexAuthPlugin(input, opts = {}) {
1616
697
  };
1617
698
  const refreshQuotaSnapshotsForAuthMenu = async () => {
1618
699
  const auth = await loadAuthStorage();
700
+ const snapshotPath = defaultSnapshotsPath();
701
+ const existingSnapshots = await loadSnapshots(snapshotPath).catch(() => ({}));
1619
702
  const snapshotUpdates = {};
1620
703
  for (const { mode, domain } of listOpenAIOAuthDomains(auth)) {
1621
704
  for (let index = 0; index < domain.accounts.length; index += 1) {
@@ -1623,11 +706,22 @@ export async function CodexAuthPlugin(input, opts = {}) {
1623
706
  if (!account || account.enabled === false)
1624
707
  continue;
1625
708
  hydrateAccountIdentityFromAccessClaims(account);
1626
- let accessToken = typeof account.access === "string" && account.access.length > 0 ? account.access : undefined;
709
+ const identityKey = account.identityKey;
1627
710
  const now = Date.now();
1628
- const expired = typeof account.expires === "number" &&
1629
- Number.isFinite(account.expires) &&
1630
- account.expires <= now;
711
+ if (identityKey) {
712
+ const cooldownUntil = quotaFetchCooldownByIdentity.get(identityKey);
713
+ if (typeof cooldownUntil === "number" && cooldownUntil > now)
714
+ continue;
715
+ const existing = existingSnapshots[identityKey];
716
+ if (existing &&
717
+ typeof existing.updatedAt === "number" &&
718
+ Number.isFinite(existing.updatedAt) &&
719
+ now - existing.updatedAt < AUTH_MENU_QUOTA_SNAPSHOT_TTL_MS) {
720
+ continue;
721
+ }
722
+ }
723
+ let accessToken = typeof account.access === "string" && account.access.length > 0 ? account.access : undefined;
724
+ const expired = typeof account.expires === "number" && Number.isFinite(account.expires) && account.expires <= now;
1631
725
  if ((!accessToken || expired) && account.refresh) {
1632
726
  try {
1633
727
  await saveAuthStorage(undefined, async (authFile) => {
@@ -1658,6 +752,9 @@ export async function CodexAuthPlugin(input, opts = {}) {
1658
752
  });
1659
753
  }
1660
754
  catch (error) {
755
+ if (identityKey) {
756
+ quotaFetchCooldownByIdentity.set(identityKey, Date.now() + AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS);
757
+ }
1661
758
  opts.log?.debug("quota check refresh failed", {
1662
759
  index,
1663
760
  mode,
@@ -1678,16 +775,20 @@ export async function CodexAuthPlugin(input, opts = {}) {
1678
775
  now: Date.now(),
1679
776
  modelFamily: "gpt-5.3-codex",
1680
777
  userAgent: resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode)),
1681
- log: opts.log
778
+ log: opts.log,
779
+ timeoutMs: AUTH_MENU_QUOTA_FETCH_TIMEOUT_MS
1682
780
  });
1683
- if (!snapshot)
781
+ if (!snapshot) {
782
+ quotaFetchCooldownByIdentity.set(account.identityKey, Date.now() + AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS);
1684
783
  continue;
784
+ }
785
+ quotaFetchCooldownByIdentity.delete(account.identityKey);
1685
786
  snapshotUpdates[account.identityKey] = snapshot;
1686
787
  }
1687
788
  }
1688
789
  if (Object.keys(snapshotUpdates).length === 0)
1689
790
  return;
1690
- await saveSnapshots(defaultSnapshotsPath(), (current) => ({
791
+ await saveSnapshots(snapshotPath, (current) => ({
1691
792
  ...current,
1692
793
  ...snapshotUpdates
1693
794
  }));
@@ -1711,7 +812,10 @@ export async function CodexAuthPlugin(input, opts = {}) {
1711
812
  handlers: {
1712
813
  onCheckQuotas: async () => {
1713
814
  await refreshQuotaSnapshotsForAuthMenu();
1714
- const report = await toolOutputForStatus();
815
+ const report = await toolOutputForStatus(undefined, undefined, {
816
+ style: "menu",
817
+ useColor: shouldUseColor()
818
+ });
1715
819
  process.stdout.write(`\n${report}\n\n`);
1716
820
  },
1717
821
  onConfigureModels: async () => {
@@ -1779,9 +883,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
1779
883
  },
1780
884
  onToggleAccount: async (account) => {
1781
885
  await saveAuthStorage(undefined, (authFile) => {
1782
- const authTypes = account.authTypes && account.authTypes.length > 0
1783
- ? [...account.authTypes]
1784
- : ["native"];
886
+ const authTypes = account.authTypes && account.authTypes.length > 0 ? [...account.authTypes] : ["native"];
1785
887
  for (const mode of authTypes) {
1786
888
  const domain = getOpenAIOAuthDomain(authFile, mode);
1787
889
  if (!domain)
@@ -1893,7 +995,9 @@ export async function CodexAuthPlugin(input, opts = {}) {
1893
995
  if (!hasOAuth)
1894
996
  return {};
1895
997
  const sessionAffinityPath = defaultSessionAffinityPath();
1896
- const loadedSessionAffinity = await loadSessionAffinity(sessionAffinityPath).catch(() => ({ version: 1 }));
998
+ const loadedSessionAffinity = await loadSessionAffinity(sessionAffinityPath).catch(() => ({
999
+ version: 1
1000
+ }));
1897
1001
  const initialSessionAffinity = readSessionAffinitySnapshot(loadedSessionAffinity, authMode);
1898
1002
  const sessionExists = createSessionExistsFn(process.env);
1899
1003
  await pruneSessionAffinitySnapshot(initialSessionAffinity, sessionExists, {
@@ -1965,9 +1069,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
1965
1069
  if (opts.headerTransformDebug === true) {
1966
1070
  await requestSnapshots.captureRequest("before-header-transform", baseRequest, {
1967
1071
  spoofMode,
1968
- ...(inboundCollaborationModeKind
1969
- ? { collaborationModeKind: inboundCollaborationModeKind }
1970
- : {})
1072
+ ...(inboundCollaborationModeKind ? { collaborationModeKind: inboundCollaborationModeKind } : {})
1971
1073
  });
1972
1074
  }
1973
1075
  let outbound = new Request(rewriteUrl(baseRequest), baseRequest);
@@ -2140,8 +1242,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2140
1242
  tokens = await refreshAccessToken(selected.refresh);
2141
1243
  }
2142
1244
  catch (error) {
2143
- if (isOAuthTokenRefreshError(error) &&
2144
- error.oauthCode?.toLowerCase() === "invalid_grant") {
1245
+ if (isOAuthTokenRefreshError(error) && error.oauthCode?.toLowerCase() === "invalid_grant") {
2145
1246
  sawInvalidGrant = true;
2146
1247
  selected.enabled = false;
2147
1248
  delete selected.cooldownUntil;
@@ -2328,6 +1429,20 @@ export async function CodexAuthPlugin(input, opts = {}) {
2328
1429
  sanitized: sanitizedOutbound.changed,
2329
1430
  ...(collaborationModeKind ? { collaborationModeKind } : {})
2330
1431
  });
1432
+ try {
1433
+ assertAllowedOutboundUrl(new URL(sanitizedOutbound.request.url));
1434
+ }
1435
+ catch (error) {
1436
+ if (isPluginFatalError(error)) {
1437
+ return toSyntheticErrorResponse(error);
1438
+ }
1439
+ return toSyntheticErrorResponse(new PluginFatalError({
1440
+ message: "Outbound request validation failed before sending to OpenAI backend.",
1441
+ status: 400,
1442
+ type: "disallowed_outbound_request",
1443
+ param: "request"
1444
+ }));
1445
+ }
2331
1446
  let response;
2332
1447
  try {
2333
1448
  response = await orchestrator.execute(sanitizedOutbound.request);
@@ -2407,7 +1522,6 @@ export async function CodexAuthPlugin(input, opts = {}) {
2407
1522
  return null;
2408
1523
  }
2409
1524
  finally {
2410
- pendingOAuth = undefined;
2411
1525
  scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
2412
1526
  }
2413
1527
  };
@@ -2445,10 +1559,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2445
1559
  };
2446
1560
  }
2447
1561
  };
2448
- if (inputs &&
2449
- process.env.OPENCODE_NO_BROWSER !== "1" &&
2450
- process.stdin.isTTY &&
2451
- process.stdout.isTTY) {
1562
+ if (inputs && process.env.OPENCODE_NO_BROWSER !== "1" && process.stdin.isTTY && process.stdout.isTTY) {
2452
1563
  return runInteractiveBrowserAuthLoop();
2453
1564
  }
2454
1565
  const { redirectUri } = await startOAuthServer();
@@ -2473,7 +1584,6 @@ export async function CodexAuthPlugin(input, opts = {}) {
2473
1584
  return { type: "failed" };
2474
1585
  }
2475
1586
  finally {
2476
- pendingOAuth = undefined;
2477
1587
  scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
2478
1588
  }
2479
1589
  }
@@ -2557,9 +1667,8 @@ export async function CodexAuthPlugin(input, opts = {}) {
2557
1667
  },
2558
1668
  "chat.message": async (hookInput, output) => {
2559
1669
  const directProviderID = hookInput.model?.providerID;
2560
- const isOpenAI = directProviderID === "openai"
2561
- || (directProviderID === undefined
2562
- && (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)));
1670
+ const isOpenAI = directProviderID === "openai" ||
1671
+ (directProviderID === undefined && (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)));
2563
1672
  if (!isOpenAI)
2564
1673
  return;
2565
1674
  for (const part of output.parts) {
@@ -2575,9 +1684,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2575
1684
  if (hookInput.model.providerID !== "openai")
2576
1685
  return;
2577
1686
  const initialReasoningEffort = asString(output.options.reasoningEffort);
2578
- const collaborationProfile = collabModeEnabled
2579
- ? resolveCollaborationProfile(hookInput.agent)
2580
- : { enabled: false };
1687
+ const collaborationProfile = collabModeEnabled ? resolveCollaborationProfile(hookInput.agent) : { enabled: false };
2581
1688
  const modelOptions = isRecord(hookInput.model.options) ? hookInput.model.options : {};
2582
1689
  const modelCandidates = getModelLookupCandidates({
2583
1690
  id: hookInput.model.id,
@@ -2633,7 +1740,12 @@ export async function CodexAuthPlugin(input, opts = {}) {
2633
1740
  });
2634
1741
  if (collabModeEnabled && collaborationProfile.enabled && collaborationProfile.kind) {
2635
1742
  const collaborationModeKind = collaborationProfile.kind;
2636
- const collaborationInstructions = resolveCollaborationInstructions(collaborationModeKind);
1743
+ const collaborationInstructions = resolveCollaborationInstructions(collaborationModeKind, {
1744
+ plan: CODEX_PLAN_MODE_INSTRUCTIONS,
1745
+ code: CODEX_CODE_MODE_INSTRUCTIONS,
1746
+ execute: CODEX_EXECUTE_MODE_INSTRUCTIONS,
1747
+ pairProgramming: CODEX_PAIR_PROGRAMMING_MODE_INSTRUCTIONS
1748
+ });
2637
1749
  const mergedInstructions = mergeInstructions(asString(output.options.instructions), collaborationInstructions);
2638
1750
  if (mergedInstructions) {
2639
1751
  output.options.instructions = mergedInstructions;
@@ -2651,9 +1763,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
2651
1763
  "chat.headers": async (hookInput, output) => {
2652
1764
  if (hookInput.model.providerID !== "openai")
2653
1765
  return;
2654
- const collaborationProfile = collabModeEnabled
2655
- ? resolveCollaborationProfile(hookInput.agent)
2656
- : { enabled: false };
1766
+ const collaborationProfile = collabModeEnabled ? resolveCollaborationProfile(hookInput.agent) : { enabled: false };
2657
1767
  const collaborationModeKind = collaborationProfile.enabled ? collaborationProfile.kind : undefined;
2658
1768
  const originator = resolveCodexOriginator(spoofMode);
2659
1769
  output.headers.originator = originator;