dsh-codex-subscription 2.1.0 → 2.1.1-beta.2

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.
package/lib/index.js CHANGED
@@ -1,18 +1,20 @@
1
- import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
2
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
3
- import { defineTool } from "@deepseek-ai/dsh-tools";
2
+ import { closeOpenAICodexWebSocketSessions, resetOpenAICodexWebSocketDebugStats } from "@earendil-works/pi-ai/api/openai-codex-responses";
3
+ import { execFile, spawn } from "node:child_process";
4
+ import { request } from "node:https";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { PassThrough, Readable } from "node:stream";
7
+ import { promisify } from "node:util";
8
+ import { HttpsProxyAgent } from "https-proxy-agent";
9
+ import WebSocket from "ws";
10
+ import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
4
11
  import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
12
+ import { defineTool } from "@deepseek-ai/dsh-tools";
5
13
  import * as dshCredentials from "@deepseek-ai/dsh-credentials";
6
14
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
15
  import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
8
16
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
9
17
  import z from "@deepseek-ai/schemastery";
10
- import { execFile, spawn } from "node:child_process";
11
- import { request } from "node:https";
12
- import { AsyncLocalStorage } from "node:async_hooks";
13
- import { Readable } from "node:stream";
14
- import { promisify } from "node:util";
15
- import { HttpsProxyAgent } from "https-proxy-agent";
16
18
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
17
19
  import { createModels } from "@earendil-works/pi-ai";
18
20
  import { WebError } from "@deepseek-ai/dsh-web";
@@ -286,6 +288,21 @@ const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt
286
288
  //#endregion
287
289
  //#region src/preference-fields.js
288
290
  const PREFERENCE_FIELDS = Object.freeze({
291
+ compactionMode: {
292
+ choices: ["dsh", "cloud"],
293
+ default: "dsh",
294
+ error: "Invalid compaction mode"
295
+ },
296
+ connectionMode: {
297
+ choices: ["sse", "websocket"],
298
+ default: "sse",
299
+ error: "Invalid connection mode"
300
+ },
301
+ subagentBackend: {
302
+ choices: ["dsh", "codex"],
303
+ default: "dsh",
304
+ error: "Invalid subagent backend"
305
+ },
289
306
  [QUICK_QUOTA_MODE_FIELD]: {
290
307
  choices: [
291
308
  "off",
@@ -330,1572 +347,2133 @@ const PREFERENCE_FIELDS = Object.freeze({
330
347
  }
331
348
  });
332
349
  //#endregion
333
- //#region src/rpc-contract.js
334
- const RPC_ENDPOINTS = Object.freeze([
335
- "status",
336
- "login/start",
337
- "login/status",
338
- "login/submit",
339
- "login/cancel",
340
- "logout",
341
- "account/select",
342
- "account/remove",
343
- "usage",
344
- "diagnostics",
345
- "preferences/status",
346
- "preferences/models",
347
- "preferences/update",
348
- "reset-credit/inspect",
349
- "reset-credit/prepare",
350
- "reset-credit/consume",
351
- "image/original/chunk",
352
- "sketch/connect",
353
- "sketch/poll",
354
- "sketch/claim",
355
- "sketch/result",
356
- "sketch/disconnect"
357
- ]);
358
- //#endregion
359
- //#region src/subscription-transport.js
360
- /** Exact routes stay inside DSH's authenticated /api bridge and body limit. */
361
- function registerSubscriptionTransport(connection, handler) {
362
- const disposers = [];
350
+ //#region src/oauth-network.js
351
+ const execFileAsync = promisify(execFile);
352
+ const CODEX_AUTH_HOST = "auth.openai.com";
353
+ const CODEX_SUBSCRIPTION_HOST = "chatgpt.com";
354
+ const CODEX_HOSTS = /* @__PURE__ */ new Set([CODEX_AUTH_HOST, CODEX_SUBSCRIPTION_HOST]);
355
+ const networkScope = new AsyncLocalStorage();
356
+ let activeScopes = 0;
357
+ let baseFetch;
358
+ let scopedFetch;
359
+ let baseWebSocket;
360
+ let scopedWebSocket;
361
+ let activeWebSocketScopes = 0;
362
+ function normalizeProxy(raw) {
363
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
364
+ const value = raw.trim().includes("://") ? raw.trim() : `http://${raw.trim()}`;
363
365
  try {
364
- for (const endpoint of RPC_ENDPOINTS) {
365
- const method = `codex-subscription/${endpoint}`;
366
- disposers.push(connection.fetch.register({
367
- path: `/api/${method}`,
368
- methods: ["POST"],
369
- requestBody: "buffered",
370
- async fetch(request) {
371
- if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
372
- let body;
373
- try {
374
- body = await request.json();
375
- } catch {
376
- return new Response("invalid JSON", { status: 400 });
377
- }
378
- const envelope = clientRequestSchema.safeParse(body);
379
- if (!envelope.success || envelope.data.method !== method) return new Response("invalid RPC envelope", { status: 400 });
380
- let result;
381
- try {
382
- request.signal.throwIfAborted();
383
- result = await handler(endpoint, envelope.data.payload, request.signal);
384
- } catch {
385
- result = {
386
- ok: false,
387
- error: {
388
- code: "internal",
389
- message: "Subscription request failed",
390
- details: { issues: [] }
391
- }
392
- };
393
- }
394
- return Response.json({
395
- type: "server-response",
396
- rpcId: envelope.data.rpcId,
397
- result
398
- });
399
- }
400
- }));
401
- }
402
- } catch (error) {
403
- for (const dispose of disposers.reverse()) dispose();
404
- throw error;
366
+ const url = new URL(value);
367
+ if (!["http:", "https:"].includes(url.protocol) || url.hostname === "") return void 0;
368
+ return url.toString();
369
+ } catch {
370
+ return;
405
371
  }
406
- return () => {
407
- for (const dispose of disposers.reverse()) dispose();
408
- };
409
372
  }
410
- //#endregion
411
- //#region src/sketch-agent-bridge.js
412
- function createSketchAgentBridge({ enabled, now = Date.now, timeoutMs = 2e4 }) {
413
- const sessions = /* @__PURE__ */ new Map();
414
- const fail = (entry, message) => {
415
- for (const task of entry.tasks.values()) task.reject(Error(message));
416
- entry.tasks.clear();
373
+ function bypassesProxy(hostname, port, rawNoProxy) {
374
+ if (typeof rawNoProxy !== "string" || rawNoProxy.trim() === "") return false;
375
+ return rawNoProxy.split(/[\s,]+/u).some((raw) => {
376
+ const entry = raw.trim().toLowerCase();
377
+ if (entry === "*") return true;
378
+ if (entry === "") return false;
379
+ const match = /^(.*?)(?::(\d+))?$/u.exec(entry);
380
+ const host = match?.[1]?.replace(/^\./u, "");
381
+ const entryPort = match?.[2];
382
+ if (!host || entryPort && entryPort !== port) return false;
383
+ return hostname === host || hostname.endsWith(`.${host}`);
384
+ });
385
+ }
386
+ function proxyFromEnvironment(env = process.env, target = new URL(`https://${CODEX_AUTH_HOST}/`)) {
387
+ if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return void 0;
388
+ return normalizeProxy(env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy);
389
+ }
390
+ function selectWindowsProxy(value) {
391
+ if (typeof value !== "string") return void 0;
392
+ const entries = value.split(";").map((item) => item.trim()).filter(Boolean);
393
+ const https = entries.find((item) => /^https=/iu.test(item));
394
+ const http = entries.find((item) => /^http=/iu.test(item));
395
+ const selected = (https ?? http ?? entries.find((item) => !item.includes("=")))?.replace(/^[^=]+=/u, "");
396
+ return normalizeProxy(selected);
397
+ }
398
+ async function windowsSystemProxy(options = {}) {
399
+ const run = options.execFile ?? execFileAsync;
400
+ const reg = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\reg.exe`;
401
+ const key = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
402
+ try {
403
+ const enabled = await run(reg, [
404
+ "query",
405
+ key,
406
+ "/v",
407
+ "ProxyEnable"
408
+ ], {
409
+ windowsHide: true,
410
+ encoding: "utf8"
411
+ });
412
+ if (!/REG_DWORD\s+0x1\b/iu.test(enabled.stdout)) return void 0;
413
+ const configured = await run(reg, [
414
+ "query",
415
+ key,
416
+ "/v",
417
+ "ProxyServer"
418
+ ], {
419
+ windowsHide: true,
420
+ encoding: "utf8"
421
+ });
422
+ return selectWindowsProxy(/^\s*ProxyServer\s+REG_\w+\s+(.+)$/imu.exec(configured.stdout)?.[1]);
423
+ } catch {
424
+ return;
425
+ }
426
+ }
427
+ async function macSystemProxy(options = {}) {
428
+ const run = options.execFile ?? execFileAsync;
429
+ try {
430
+ const result = await run("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8" });
431
+ if (!/^\s*HTTPSEnable\s*:\s*1\s*$/imu.test(result.stdout)) return void 0;
432
+ const host = /^\s*HTTPSProxy\s*:\s*(\S+)\s*$/imu.exec(result.stdout)?.[1];
433
+ const port = /^\s*HTTPSPort\s*:\s*(\d+)\s*$/imu.exec(result.stdout)?.[1];
434
+ return normalizeProxy(host && port ? `${host}:${port}` : void 0);
435
+ } catch {
436
+ return;
437
+ }
438
+ }
439
+ async function resolveCodexOAuthProxy(options = {}) {
440
+ return (await resolveCodexProxy(options)).url;
441
+ }
442
+ async function resolveCodexProxy(options = {}) {
443
+ const target = options.target ?? new URL(`https://${CODEX_AUTH_HOST}/`);
444
+ const env = options.env ?? process.env;
445
+ if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return {
446
+ url: void 0,
447
+ source: "bypass"
417
448
  };
418
- const find = (payload) => {
419
- const entry = sessions.get(payload.sessionId);
420
- if (!entry || entry.token !== payload.token || now() - entry.seen > 1e4) throw Error("Sketch connection expired");
421
- entry.seen = now();
422
- return entry;
449
+ const envProxy = proxyFromEnvironment(env, target);
450
+ if (envProxy) return {
451
+ url: envProxy,
452
+ source: "environment"
423
453
  };
424
- return {
425
- async rpc(endpoint, payload) {
426
- try {
427
- if (!enabled()) throw Error("Sketch is disabled");
428
- if (!payload || typeof payload.sessionId !== "string" || !payload.sessionId.length || payload.sessionId.length > 200) throw Error("Invalid session");
429
- if (endpoint === "sketch/connect") {
430
- for (const [id, entry] of sessions) if (now() - entry.seen >= 1e4) {
431
- fail(entry, "Sketch connection expired");
432
- sessions.delete(id);
433
- }
434
- const previous = sessions.get(payload.sessionId);
435
- if (previous && now() - previous.seen < 1e4) throw Error("Another board is connected to this session");
436
- if (previous) fail(previous, "Sketch connection replaced");
437
- const entry = {
438
- token: randomUUID(),
439
- seen: now(),
440
- tasks: /* @__PURE__ */ new Map(),
441
- cancelled: []
442
- };
443
- sessions.set(payload.sessionId, entry);
444
- return {
445
- ok: true,
446
- value: { token: entry.token }
447
- };
448
- }
449
- const entry = find(payload);
450
- if (endpoint === "sketch/poll") return {
451
- ok: true,
452
- value: [...entry.cancelled.splice(0).map((id) => ({
453
- id,
454
- cancelled: true
455
- })), ...[...entry.tasks].filter(([, t]) => !t.delivered).map(([id, t]) => {
456
- t.delivered = true;
457
- return {
458
- id,
459
- request: t.request,
460
- expiresAt: t.expiresAt
461
- };
462
- })]
463
- };
464
- if (endpoint === "sketch/claim") {
465
- const task = entry.tasks.get(payload.id);
466
- return {
467
- ok: true,
468
- value: Boolean(task && task.delivered && task.expiresAt > now())
469
- };
470
- }
471
- if (endpoint === "sketch/disconnect") {
472
- fail(entry, "Sketch board closed");
473
- sessions.delete(payload.sessionId);
474
- return {
475
- ok: true,
476
- value: null
477
- };
478
- }
479
- if (endpoint === "sketch/result") {
480
- const task = entry.tasks.get(payload.id);
481
- if (task) {
482
- entry.tasks.delete(payload.id);
483
- payload.error ? task.reject(Error(String(payload.error).slice(0, 500))) : task.resolve(payload.value);
484
- }
485
- return {
486
- ok: true,
487
- value: null
488
- };
489
- }
490
- throw Error("Unknown sketch route");
491
- } catch (error) {
492
- return {
493
- ok: false,
494
- error: {
495
- code: "invalid-input",
496
- message: error.message,
497
- details: { issues: [] }
498
- }
499
- };
500
- }
501
- },
502
- request(sessionId, request, signal) {
503
- if (!enabled()) return Promise.reject(Error("Sketch is disabled"));
504
- const entry = sessions.get(sessionId);
505
- if (!entry || now() - entry.seen > 1e4) return Promise.reject(Error("Switch to this session in DSH with sketch editing enabled"));
506
- if (entry.tasks.size) return Promise.reject(Error("Another sketch operation is pending"));
507
- if (JSON.stringify(request).length > 2e6) return Promise.reject(Error("Sketch batch is too large"));
508
- return new Promise((resolve, reject) => {
509
- const id = randomUUID();
510
- const finish = (callback, value) => {
511
- clearTimeout(timer);
512
- signal?.removeEventListener("abort", abort);
513
- entry.tasks.delete(id);
514
- callback(value);
515
- };
516
- const cancel = (message) => {
517
- if (entry.tasks.get(id)?.delivered) {
518
- entry.cancelled.push(id);
519
- if (entry.cancelled.length > 32) entry.cancelled.shift();
520
- }
521
- finish(reject, Error(message));
522
- };
523
- const abort = () => cancel("Sketch operation interrupted; inspect recentRequests before retrying");
524
- const timer = setTimeout(() => cancel("Sketch response timed out; inspect recentRequests before retrying"), timeoutMs);
525
- entry.tasks.set(id, {
526
- request,
527
- expiresAt: now() + timeoutMs,
528
- delivered: false,
529
- resolve: (value) => finish(resolve, value),
530
- reject: (error) => finish(reject, error)
454
+ const platform = options.platform ?? process.platform;
455
+ const system = platform === "win32" ? await windowsSystemProxy(options) : platform === "darwin" ? await macSystemProxy(options) : void 0;
456
+ return system ? {
457
+ url: system,
458
+ source: "system"
459
+ } : {
460
+ url: void 0,
461
+ source: "direct"
462
+ };
463
+ }
464
+ function bodyBytes(body) {
465
+ if (body === void 0 || body === null) return void 0;
466
+ if (typeof body === "string") return Buffer.from(body);
467
+ if (body instanceof URLSearchParams) return Buffer.from(body.toString());
468
+ if (body instanceof Uint8Array) return Buffer.from(body);
469
+ throw new TypeError("Unsupported Codex OAuth request body");
470
+ }
471
+ function fetchThroughProxy(input, init, proxyUrl) {
472
+ const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
473
+ const body = bodyBytes(init?.body);
474
+ const headers = new Headers(init?.headers);
475
+ if (body && !headers.has("content-length")) headers.set("content-length", String(body.byteLength));
476
+ return new Promise((resolve, reject) => {
477
+ const request$1 = request(target, {
478
+ method: init?.method ?? "GET",
479
+ headers: Object.fromEntries(headers.entries()),
480
+ agent: new HttpsProxyAgent(proxyUrl),
481
+ signal: init?.signal
482
+ }, (response) => {
483
+ const responseHeaders = new Headers();
484
+ for (const [name, value] of Object.entries(response.headers)) if (Array.isArray(value)) value.forEach((item) => responseHeaders.append(name, item));
485
+ else if (value !== void 0) responseHeaders.set(name, value);
486
+ const status = response.statusCode ?? 500;
487
+ const empty = init?.method === "HEAD" || [
488
+ 204,
489
+ 205,
490
+ 304
491
+ ].includes(status);
492
+ resolve(new Response(empty ? null : Readable.toWeb(response), {
493
+ status,
494
+ statusText: response.statusMessage,
495
+ headers: responseHeaders
496
+ }));
497
+ });
498
+ request$1.on("error", reject);
499
+ if (body) request$1.write(body);
500
+ request$1.end();
501
+ });
502
+ }
503
+ async function withCodexNetwork(run, options = {}) {
504
+ if (options.websocket && activeWebSocketScopes === 0) {
505
+ baseWebSocket = globalThis.WebSocket;
506
+ scopedWebSocket = new Proxy(baseWebSocket ?? WebSocket, { construct(target, args, newTarget) {
507
+ const scope = networkScope.getStore();
508
+ const url = new URL(String(args[0]));
509
+ if (!scope?.options.websocket || url.protocol !== "wss:" || url.hostname !== CODEX_SUBSCRIPTION_HOST) return Reflect.construct(target, args, newTarget);
510
+ const proxy = scope.options.websocketProxy;
511
+ return new WebSocket(args[0], {
512
+ ...args[1],
513
+ ...proxy ? { agent: new HttpsProxyAgent(proxy) } : {}
514
+ });
515
+ } });
516
+ globalThis.WebSocket = scopedWebSocket;
517
+ }
518
+ if (options.websocket) activeWebSocketScopes += 1;
519
+ if (activeScopes === 0) {
520
+ baseFetch = globalThis.fetch;
521
+ scopedFetch = async (input, init) => {
522
+ const scope = networkScope.getStore();
523
+ if (scope === void 0) return baseFetch(input, init);
524
+ const { options: scopedOptions, allowedHosts, resolved } = scope;
525
+ const proxyFetch = scopedOptions.fetchThroughProxy ?? fetchThroughProxy;
526
+ const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
527
+ if (target.protocol !== "https:" || !allowedHosts.has(target.hostname)) return baseFetch(input, init);
528
+ let proxy = resolved.get(target.hostname);
529
+ if (proxy === void 0) {
530
+ proxy = resolveCodexProxy({
531
+ ...scopedOptions,
532
+ target
531
533
  });
532
- signal?.addEventListener("abort", abort, { once: true });
533
- if (signal?.aborted) abort();
534
+ resolved.set(target.hostname, proxy);
535
+ }
536
+ const route = await proxy;
537
+ scopedOptions.onRoute?.(route.source);
538
+ const response = await (route.url === void 0 ? baseFetch(input, init) : proxyFetch(input, init, route.url));
539
+ return scopedOptions.transformResponse?.(response, target) ?? response;
540
+ };
541
+ globalThis.fetch = scopedFetch;
542
+ }
543
+ activeScopes += 1;
544
+ const scope = {
545
+ options,
546
+ allowedHosts: options.hosts ?? CODEX_HOSTS,
547
+ resolved: /* @__PURE__ */ new Map()
548
+ };
549
+ try {
550
+ return await networkScope.run(scope, run);
551
+ } finally {
552
+ if (options.websocket && --activeWebSocketScopes === 0) {
553
+ if (globalThis.WebSocket === scopedWebSocket) globalThis.WebSocket = baseWebSocket;
554
+ baseWebSocket = void 0;
555
+ scopedWebSocket = void 0;
556
+ }
557
+ activeScopes -= 1;
558
+ if (activeScopes === 0) {
559
+ if (globalThis.fetch === scopedFetch) globalThis.fetch = baseFetch;
560
+ baseFetch = void 0;
561
+ scopedFetch = void 0;
562
+ }
563
+ }
564
+ }
565
+ function classifyTransportError(error) {
566
+ const name = error?.name;
567
+ const code = String(error?.code ?? error?.cause?.code ?? "");
568
+ if (name === "AbortError" || name === "TimeoutError" || /ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT/u.test(code)) return "timeout";
569
+ if (/ENOTFOUND|EAI_AGAIN/u.test(code)) return "dns";
570
+ if (/CERT_|TLS|SSL/u.test(code)) return "tls";
571
+ if (/ECONN|EPIPE|UND_ERR_SOCKET/u.test(code)) return "connection";
572
+ return "network";
573
+ }
574
+ const elapsedBucket = (elapsed) => elapsed < 1e3 ? "under-1s" : elapsed < 5e3 ? "1-5s" : elapsed < 15e3 ? "5-15s" : "over-15s";
575
+ function createCodexNetworkTransport(options = {}) {
576
+ const attempts = /* @__PURE__ */ new Map();
577
+ const now = options.now ?? Date.now;
578
+ const run = async (area, operation, connection = {}) => {
579
+ const startedAt = now();
580
+ let route = attempts.get(area)?.route ?? "direct";
581
+ let routed = false;
582
+ try {
583
+ const value = await withCodexNetwork(operation, {
584
+ ...options,
585
+ ...connection,
586
+ onRoute: (source) => {
587
+ route = source;
588
+ routed = true;
589
+ }
590
+ });
591
+ if (value instanceof Response && !value.ok) attempts.set(area, {
592
+ status: "failed",
593
+ stage: "http",
594
+ code: "http-error",
595
+ httpStatus: value.status,
596
+ route,
597
+ elapsed: elapsedBucket(now() - startedAt)
598
+ });
599
+ else if (routed || value instanceof Response) attempts.set(area, {
600
+ status: "ok",
601
+ route,
602
+ elapsed: elapsedBucket(now() - startedAt)
534
603
  });
604
+ return value;
605
+ } catch (error) {
606
+ if (routed) attempts.set(area, {
607
+ status: "failed",
608
+ stage: "transport",
609
+ code: classifyTransportError(error),
610
+ route,
611
+ elapsed: elapsedBucket(now() - startedAt)
612
+ });
613
+ throw error;
614
+ }
615
+ };
616
+ return Object.freeze({
617
+ run,
618
+ fetch: (area, input, init) => run(area, () => globalThis.fetch(input, init)),
619
+ snapshot: () => Object.fromEntries([...attempts].map(([area, value]) => [area, { ...value }]))
620
+ });
621
+ }
622
+ //#endregion
623
+ //#region src/subscription-connection.js
624
+ function createSubscriptionConnection({ resolveMode = () => "sse", resolveProxy = resolveCodexOAuthProxy } = {}) {
625
+ const namespace = randomUUID();
626
+ const sessions = /* @__PURE__ */ new Set();
627
+ return {
628
+ async prepare(options = {}) {
629
+ if (resolveMode() !== "websocket") return { options: {
630
+ ...options,
631
+ transport: "sse"
632
+ } };
633
+ const proxy = await resolveProxy({ target: new URL("https://chatgpt.com/") });
634
+ const sessionId = options.sessionId && `dsh-${createHash("sha256").update(JSON.stringify([
635
+ namespace,
636
+ options.sessionId,
637
+ options.apiKey,
638
+ proxy
639
+ ])).digest("hex").slice(0, 56)}`;
640
+ if (sessionId) sessions.add(sessionId);
641
+ return {
642
+ options: {
643
+ ...options,
644
+ sessionId,
645
+ transport: "websocket-cached",
646
+ websocketConnectTimeoutMs: 1e4,
647
+ env: {}
648
+ },
649
+ network: {
650
+ websocket: true,
651
+ websocketProxy: proxy
652
+ }
653
+ };
535
654
  },
536
655
  dispose() {
537
- for (const entry of sessions.values()) fail(entry, "Sketch service stopped");
656
+ for (const session of sessions) {
657
+ closeOpenAICodexWebSocketSessions(session);
658
+ resetOpenAICodexWebSocketDebugStats(session);
659
+ }
538
660
  sessions.clear();
539
661
  }
540
662
  };
541
663
  }
542
664
  //#endregion
543
- //#region src/sketch-command-schema.js
544
- const number = { type: "number" };
545
- const string = { type: "string" };
546
- const object = (properties) => ({
547
- type: "object",
548
- additionalProperties: false,
549
- properties
550
- });
551
- const points = {
552
- type: "array",
553
- items: object({
554
- x: {
555
- ...number,
556
- required: true
557
- },
558
- y: {
559
- ...number,
560
- required: true
561
- }
562
- })
563
- };
564
- const point = object({
565
- x: {
566
- ...number,
567
- required: true
568
- },
569
- y: {
570
- ...number,
571
- required: true
665
+ //#region src/subscription-compaction.js
666
+ /** Experimental SSE bridge; callers must explicitly opt in. */
667
+ const clone$2 = (value) => JSON.parse(JSON.stringify(value));
668
+ const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
669
+ const key = "codexCompactionV1";
670
+ const historyHash = (messages) => hash(messages.map(({ source, ...message }) => ({
671
+ ...message,
672
+ source: source && {
673
+ kind: source.kind,
674
+ provider: source.provider,
675
+ model: source.model
572
676
  }
573
- });
574
- const style = {
575
- color: string,
576
- width: number,
577
- opacity: number,
578
- fill: { type: "boolean" },
579
- text: string,
580
- points
581
- };
582
- const sketchCommandArray = {
583
- type: "array",
584
- items: object({
585
- op: {
586
- type: "string",
587
- required: true,
588
- enum: [
589
- "stroke",
590
- "object",
591
- "layer",
592
- "resize"
593
- ]
594
- },
595
- id: {
596
- oneOf: [{ type: "string" }, { type: "integer" }],
597
- description: "Object string ID. Layer add: optional NEW unique integer ID; other layer actions: existing layer ID."
598
- },
599
- after: {
600
- type: "integer",
601
- description: "Layer add only: existing layer to insert after; defaults to active."
602
- },
603
- start: point,
604
- segments: {
605
- type: "array",
606
- items: object({
607
- control1: {
608
- ...point,
609
- required: true
610
- },
611
- control2: {
612
- ...point,
613
- required: true
614
- },
615
- end: {
616
- ...point,
617
- required: true
618
- }
619
- })
620
- },
621
- layer: { type: "integer" },
622
- shape: {
623
- type: "string",
624
- enum: [
625
- "pen",
626
- "line",
627
- "arrow",
628
- "text",
629
- "rectangle",
630
- "circle",
631
- "ellipse",
632
- "polygon",
633
- "bezier",
634
- "eraser"
635
- ]
636
- },
637
- ...style,
638
- action: {
639
- type: "string",
640
- enum: [
641
- "update",
642
- "duplicate",
643
- "delete",
644
- "add",
645
- "select",
646
- "rename",
647
- "visible",
648
- "up",
649
- "down",
650
- "clear"
651
- ]
652
- },
653
- value: string,
654
- ratio: {
655
- type: "string",
656
- enum: [
657
- "1:1",
658
- "4:3",
659
- "3:4",
660
- "16:9",
661
- "9:16"
662
- ]
663
- },
664
- patch: object(style),
665
- transform: object({
666
- dx: number,
667
- dy: number,
668
- scaleX: number,
669
- scaleY: number
670
- })
671
- })
672
- };
673
- //#endregion
674
- //#region src/sketch-agent-tool.js
675
- function createSketchAgentTool(bridge, attachments) {
676
- return defineTool({
677
- name: "codex_sketch",
678
- description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
679
- parameters: {
680
- action: {
681
- type: "string",
682
- required: true,
683
- enum: [
684
- "inspect",
685
- "apply",
686
- "preview",
687
- "save",
688
- "finish"
689
- ]
690
- },
691
- runId: {
692
- type: "string",
693
- description: "From inspect; required for all other actions. Never reuse a stopped run."
694
- },
695
- documentId: {
696
- type: "string",
697
- description: "From inspect; required except for inspect."
698
- },
699
- revision: {
700
- type: "integer",
701
- description: "From latest response; required for apply/save/finish."
702
- },
703
- requestId: {
704
- type: "string",
705
- description: "Unique id for apply/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
706
- },
707
- commands: {
708
- oneOf: [sketchCommandArray, { type: "string" }],
709
- description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
710
- },
711
- name: {
712
- type: "string",
713
- description: "Draft name for save/finish."
714
- },
715
- offset: {
716
- type: "integer",
717
- description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
718
- },
719
- objectId: {
720
- type: "string",
721
- description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
722
- },
723
- layer: {
724
- type: "integer",
725
- description: "inspect only: layer containing objectId."
726
- }
727
- },
728
- timeoutMs: 25e3,
729
- isConcurrencySafe: () => false,
730
- async execute(args, exec) {
731
- const sessionId = exec.agent?.id;
732
- if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
733
- const request = { ...args };
734
- if (args.action === "apply") try {
735
- request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
736
- if (!Array.isArray(request.commands)) throw Error();
737
- } catch {
738
- throw Error("commands must be a native array or JSON array string");
677
+ })));
678
+ const MAX_BYTES = 2 * 1024 * 1024;
679
+ const MAX_AGE_MS = 10080 * 60 * 1e3;
680
+ function createCompactionBridge({ enabled = () => false, threshold = () => 1e5, accountScope, diagnostic = () => {}, now = Date.now }) {
681
+ const scope = new AsyncLocalStorage();
682
+ const wrapStream = (factory, options) => (async function* () {
683
+ if (!enabled() || options.provider !== "openai-codex") {
684
+ yield* factory(options);
685
+ return;
686
+ }
687
+ const identity = await accountScope(options);
688
+ if (!identity) throw new Error("Compaction requires a stable account identity");
689
+ const state = {
690
+ identity: hash([identity, options.model]),
691
+ original: options.messages,
692
+ blocks: [],
693
+ suffix: void 0,
694
+ completed: false
695
+ };
696
+ let messages = options.messages;
697
+ for (let i = messages.length - 1; i >= 0; i--) {
698
+ const candidate = messages[i].source?.replayState?.response?.[key];
699
+ if (!candidate) continue;
700
+ if (Number.isFinite(candidate.createdAt) && candidate.createdAt <= now() && now() - candidate.createdAt <= MAX_AGE_MS && candidate.scope === state.identity && candidate.prefix === historyHash(messages.slice(0, i)) && candidate.content === hash(messages[i].content) && Array.isArray(candidate.items) && candidate.items[0]?.type === "compaction" && typeof candidate.items[0].encrypted_content === "string" && candidate.digest === hash(candidate.items) && JSON.stringify(candidate.items).length <= MAX_BYTES) {
701
+ state.suffix = clone$2(candidate.items);
702
+ messages = [...messages.slice(0, i + 1).filter((m) => m.role === "system"), ...messages.slice(i + 1)];
739
703
  }
740
- const value = await bridge.request(sessionId, request, exec.signal);
741
- if (value.png) {
742
- if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
743
- const image = await attachments.saveImage({
744
- data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
745
- mediaType: "image/png",
746
- name: "sketch-preview.png"
704
+ break;
705
+ }
706
+ const iterator = scope.run(state, () => factory({
707
+ ...options,
708
+ messages
709
+ })[Symbol.asyncIterator]());
710
+ try {
711
+ while (true) {
712
+ const result = await scope.run(state, () => iterator.next());
713
+ if (result.done) break;
714
+ let event = result.value;
715
+ if (event.type === "block-end") state.blocks[event.index] = clone$2(event.block);
716
+ if (event.type === "finish") diagnostic({
717
+ completed: state.completed,
718
+ captured: state.captured?.length ?? 0,
719
+ replay: !!event.replayState
747
720
  });
748
- const { png, ...snapshot } = value;
749
- return {
750
- ...snapshot,
751
- image
752
- };
721
+ if (event.type === "finish" && ["stop", "tool-calls"].includes(event.reason?.kind) && !options.signal?.aborted && state.completed && state.captured?.length && event.replayState) {
722
+ const items = state.captured;
723
+ const checkpoint = {
724
+ createdAt: now(),
725
+ scope: state.identity,
726
+ prefix: historyHash(state.original),
727
+ content: hash(state.blocks.filter(Boolean)),
728
+ items,
729
+ digest: hash(items)
730
+ };
731
+ event = {
732
+ ...event,
733
+ replayState: {
734
+ ...event.replayState,
735
+ response: {
736
+ ...event.replayState.response,
737
+ [key]: checkpoint
738
+ }
739
+ }
740
+ };
741
+ }
742
+ yield event;
753
743
  }
754
- return value;
755
- },
756
- output: {
757
- schema: {
758
- type: "object",
759
- additionalProperties: true
760
- },
761
- render: (_args, value) => [{
762
- type: "text",
763
- text: JSON.stringify({
764
- ...value,
765
- image: void 0
766
- })
767
- }, ...value.image ? [{
768
- type: "image",
769
- attachment: value.image
770
- }] : []]
744
+ } finally {
745
+ await scope.run(state, () => iterator.return?.());
771
746
  }
772
- });
773
- }
774
- //#endregion
775
- //#region src/sketch-codec-route.js
776
- function registerSketchCodec(connection) {
777
- let source;
778
- return connection.fetch.register({
779
- path: "/api/codex-subscription/sketch-psd-worker",
780
- methods: ["GET"],
781
- requestBody: "buffered",
782
- async fetch() {
783
- source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
784
- return new Response(source, { headers: {
785
- "content-type": "text/javascript; charset=utf-8",
786
- "cache-control": "no-store"
747
+ })();
748
+ return {
749
+ wrapAdapter(adapter) {
750
+ return new Proxy(adapter, { get(target, property) {
751
+ if (property === "stream") return (options) => wrapStream((o) => target.stream(o), options);
752
+ if (property === "prepareCall") return async (...args) => {
753
+ const call = await target.prepareCall(...args);
754
+ return {
755
+ ...call,
756
+ stream: (options) => wrapStream((o) => call.stream(o), options)
757
+ };
758
+ };
759
+ const value = Reflect.get(target, property, target);
760
+ return typeof value === "function" ? value.bind(target) : value;
787
761
  } });
762
+ },
763
+ preparePayload(payload, contextWindow) {
764
+ const state = scope.getStore();
765
+ if (!state) return payload;
766
+ const configured = threshold();
767
+ if (!Number.isSafeInteger(configured) || configured < 1e3) throw new Error("Invalid compaction threshold");
768
+ const limit = Number.isFinite(contextWindow) && contextWindow >= 2e3 ? Math.min(configured, Math.floor(contextWindow / 2)) : configured;
769
+ if (!Number.isSafeInteger(limit) || limit < 1e3) throw new Error("Invalid compaction threshold");
770
+ return {
771
+ ...payload,
772
+ input: [...state.suffix ?? [], ...payload.input],
773
+ context_management: [{
774
+ type: "compaction",
775
+ compact_threshold: limit
776
+ }]
777
+ };
778
+ },
779
+ requestOptions(options) {
780
+ return scope.getStore() ? {
781
+ ...options,
782
+ transport: "sse"
783
+ } : options;
784
+ },
785
+ networkOptions(options) {
786
+ const state = scope.getStore();
787
+ if (!state) return options;
788
+ return {
789
+ ...options,
790
+ websocket: false,
791
+ transformResponse: (response, target) => {
792
+ if (target.hostname !== "chatgpt.com" || target.pathname !== "/backend-api/codex/responses" || !response.ok || !response.body) return response;
793
+ state.completed = false;
794
+ state.captured = void 0;
795
+ let buffer = "", oversized = false;
796
+ const decoder = new TextDecoder();
797
+ const observe = (line) => {
798
+ if (!line.startsWith("data:")) return;
799
+ const data = line.slice(5).trim();
800
+ if (data === "[DONE]") return;
801
+ let event;
802
+ try {
803
+ event = JSON.parse(data);
804
+ } catch {
805
+ return;
806
+ }
807
+ if (event.type === "response.output_item.done") {
808
+ if (event.item?.type === "compaction") state.captured = [clone$2(event.item)];
809
+ else if (state.captured) state.captured.push(clone$2(event.item));
810
+ if (state.captured && JSON.stringify(state.captured).length > MAX_BYTES) {
811
+ state.captured = void 0;
812
+ oversized = true;
813
+ }
814
+ }
815
+ if (event.type === "response.completed" && event.response?.status === "completed") state.completed = !oversized;
816
+ };
817
+ const stream = response.body.pipeThrough(new TransformStream({
818
+ transform(bytes, controller) {
819
+ controller.enqueue(bytes);
820
+ if (oversized) return;
821
+ buffer += decoder.decode(bytes, { stream: true });
822
+ let index;
823
+ while ((index = buffer.indexOf("\n")) >= 0) {
824
+ observe(buffer.slice(0, index).trimEnd());
825
+ buffer = buffer.slice(index + 1);
826
+ }
827
+ if (buffer.length > MAX_BYTES) {
828
+ buffer = "";
829
+ oversized = true;
830
+ state.captured = void 0;
831
+ }
832
+ },
833
+ flush() {
834
+ buffer += decoder.decode();
835
+ if (!oversized && buffer) observe(buffer.trimEnd());
836
+ }
837
+ }));
838
+ return new Response(stream, {
839
+ status: response.status,
840
+ statusText: response.statusText,
841
+ headers: response.headers
842
+ });
843
+ }
844
+ };
788
845
  }
789
- });
846
+ };
790
847
  }
791
848
  //#endregion
792
- //#region src/account-vault.js
793
- const VERSION = 1;
794
- const DEFAULT_LABEL = "Account 1";
795
- const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
796
- const EMAIL_MAX_LENGTH = 254;
797
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
798
- /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
799
- function normalizeAccountEmail(value) {
800
- if (typeof value !== "string") return void 0;
801
- const email = value.trim();
802
- return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
803
- }
804
- function decodeJwtPayload(access) {
805
- if (typeof access !== "string") return void 0;
806
- const encoded = access.split(".")[1];
807
- if (typeof encoded !== "string" || encoded.length === 0) return void 0;
808
- try {
809
- return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
810
- } catch {
811
- return;
812
- }
813
- }
814
- function emailFromAccessToken(access) {
815
- const payload = decodeJwtPayload(access);
816
- return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
817
- }
818
- /** Normalize the one non-secret account attribute that may cross the UI boundary. */
819
- function sanitizeOAuthCredential(value) {
820
- const credential = assertOAuthCredential$1(value);
821
- const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
822
- if (email === void 0) {
823
- delete credential.email;
824
- return credential;
825
- }
826
- return {
827
- ...credential,
828
- email
829
- };
830
- }
831
- function assertOAuthCredential$1(value) {
832
- if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
833
- return clone$1(value);
834
- }
835
- function parseOAuthCredential$1(value) {
849
+ //#region src/rpc-contract.js
850
+ const RPC_ENDPOINTS = Object.freeze([
851
+ "status",
852
+ "login/start",
853
+ "login/status",
854
+ "login/submit",
855
+ "login/cancel",
856
+ "logout",
857
+ "account/select",
858
+ "account/remove",
859
+ "usage",
860
+ "diagnostics",
861
+ "preferences/status",
862
+ "preferences/models",
863
+ "preferences/update",
864
+ "reset-credit/inspect",
865
+ "reset-credit/prepare",
866
+ "reset-credit/consume",
867
+ "image/original/chunk",
868
+ "sketch/connect",
869
+ "sketch/poll",
870
+ "sketch/claim",
871
+ "sketch/result",
872
+ "sketch/disconnect"
873
+ ]);
874
+ //#endregion
875
+ //#region src/subscription-transport.js
876
+ /** Exact routes stay inside DSH's authenticated /api bridge and body limit. */
877
+ function registerSubscriptionTransport(connection, handler) {
878
+ const disposers = [];
836
879
  try {
837
- return assertOAuthCredential$1(JSON.parse(value));
880
+ for (const endpoint of RPC_ENDPOINTS) {
881
+ const method = `codex-subscription/${endpoint}`;
882
+ disposers.push(connection.fetch.register({
883
+ path: `/api/${method}`,
884
+ methods: ["POST"],
885
+ requestBody: "buffered",
886
+ async fetch(request) {
887
+ if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
888
+ let body;
889
+ try {
890
+ body = await request.json();
891
+ } catch {
892
+ return new Response("invalid JSON", { status: 400 });
893
+ }
894
+ const envelope = clientRequestSchema.safeParse(body);
895
+ if (!envelope.success || envelope.data.method !== method) return new Response("invalid RPC envelope", { status: 400 });
896
+ let result;
897
+ try {
898
+ request.signal.throwIfAborted();
899
+ result = await handler(endpoint, envelope.data.payload, request.signal);
900
+ } catch {
901
+ result = {
902
+ ok: false,
903
+ error: {
904
+ code: "internal",
905
+ message: "Subscription request failed",
906
+ details: { issues: [] }
907
+ }
908
+ };
909
+ }
910
+ return Response.json({
911
+ type: "server-response",
912
+ rpcId: envelope.data.rpcId,
913
+ result
914
+ });
915
+ }
916
+ }));
917
+ }
838
918
  } catch (error) {
839
- if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
840
- throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
919
+ for (const dispose of disposers.reverse()) dispose();
920
+ throw error;
841
921
  }
922
+ return () => {
923
+ for (const dispose of disposers.reverse()) dispose();
924
+ };
842
925
  }
843
- function normalizeLabel(value) {
844
- if (typeof value !== "string") throw new Error("Codex account label must be text");
845
- const label = value.trim().replace(/\s+/gu, " ");
846
- if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
847
- return label;
848
- }
849
- function assertVaultRecord(record) {
850
- if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
851
- const ids = /* @__PURE__ */ new Set();
852
- const accounts = record.payload.accounts.map((account) => {
853
- if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
854
- ids.add(account.id);
926
+ //#endregion
927
+ //#region src/subagent-auth.js
928
+ const AUTH_ERROR = "Codex subscription authorization failed; check the selected account";
929
+ /** Keep refresh rotation in the plugin's existing serialized credential store. */
930
+ async function createSubagentTokens({ resolveAuth, store, refresh, signal }) {
931
+ signal.throwIfAborted();
932
+ await resolveAuth();
933
+ const initial = await store.read("openai-codex", { signal });
934
+ if (initial?.type !== "oauth" || !initial.accountId || !initial.access) throw new Error(AUTH_ERROR);
935
+ const accountId = initial.accountId;
936
+ let access = initial.access;
937
+ return async (previousAccountId, forceRefresh = false) => {
938
+ signal.throwIfAborted();
939
+ if (previousAccountId !== void 0 && previousAccountId !== accountId) throw new Error(AUTH_ERROR);
940
+ let credential;
941
+ if (previousAccountId !== void 0 || forceRefresh) {
942
+ const rejectedAccess = access;
943
+ credential = await store.modify("openai-codex", async (current) => {
944
+ if (current?.accountId !== accountId) throw new Error(AUTH_ERROR);
945
+ if (current.access !== rejectedAccess) return current;
946
+ const next = await refresh(current);
947
+ if (next?.accountId !== accountId) throw new Error(AUTH_ERROR);
948
+ return next;
949
+ }, { signal });
950
+ } else credential = await store.read("openai-codex", { signal });
951
+ signal.throwIfAborted();
952
+ if (credential?.accountId !== accountId || !credential.access) throw new Error(AUTH_ERROR);
953
+ access = credential.access;
855
954
  return {
856
- id: account.id,
857
- label: normalizeLabel(account.label),
858
- credential: sanitizeOAuthCredential(account.credential)
955
+ accessToken: access,
956
+ chatgptAccountId: accountId
859
957
  };
860
- });
861
- if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
862
- const legacyAccountId = record.payload.legacyAccountId;
863
- if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
864
- return {
865
- version: VERSION,
866
- activeId: record.payload.activeId,
867
- legacyAccountId,
868
- accounts
869
958
  };
870
959
  }
871
- const grant = (payload) => ({
872
- kind: "grant",
873
- payload
874
- });
875
- var PendingOAuthCredentialStore = class {
876
- #credential;
877
- async read(providerId) {
878
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
879
- return clone$1(this.#credential);
880
- }
881
- async list() {
882
- return this.#credential === void 0 ? [] : [{
883
- providerId: "openai-codex",
884
- type: "oauth"
885
- }];
886
- }
887
- async modify(providerId, update) {
888
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
889
- const next = await update(clone$1(this.#credential));
890
- if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
891
- return clone$1(this.#credential);
892
- }
893
- async delete(providerId) {
894
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
895
- this.#credential = void 0;
896
- }
897
- credential() {
898
- return clone$1(this.#credential);
899
- }
900
- };
901
960
  /**
902
- * Multi-account owner state stored in DSH's atomic plugin credential record.
903
- * The old single-account reference remains as a rollback source and is kept in
904
- * sync whenever that imported account rotates its refresh token.
961
+ * Authenticate the official DSH provider's private app-server connection.
962
+ * DSH still owns framing, process containment, turns, tool approvals and disposal.
963
+ * Only the documented external-auth handshake and thread policy are adapted.
964
+ * No token is passed in argv, environment, logs or a second auth.json.
905
965
  */
906
- var DshOAuthAccountVault = class {
907
- #tail = Promise.resolve();
908
- constructor(credentials, options) {
909
- if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
910
- this.credentials = credentials;
911
- this.key = options.key;
912
- this.legacyRef = options.legacyRef;
913
- this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
914
- this.createId = options.createId ?? randomUUID;
915
- this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
916
- }
917
- #enqueue(operation) {
918
- const current = this.#tail.catch(() => void 0).then(operation);
919
- this.#tail = current.catch(() => void 0);
920
- return current;
921
- }
922
- async #legacyCredential() {
923
- for (const ref of [this.legacyRef, ...this.legacyRefs]) {
924
- const hit = await this.credentials.resolve(ref);
925
- if (hit?.value === void 0 || hit.value === "") continue;
926
- return {
927
- ref,
928
- credential: parseOAuthCredential$1(hit.value)
929
- };
966
+ function authenticatedSubagentChild(child, { Transport, getTokens, thread, signal }) {
967
+ const stdin = new PassThrough();
968
+ const stdout = new PassThrough();
969
+ const host = new Transport(stdin, stdout);
970
+ const server = new Transport(child.stdout, child.stdin);
971
+ let initialized = false;
972
+ let authenticated;
973
+ let closed = false;
974
+ const close = () => {
975
+ if (closed) return;
976
+ closed = true;
977
+ host.close();
978
+ server.close();
979
+ stdin.destroy();
980
+ stdout.end();
981
+ };
982
+ const authorize = async (previousAccountId, refresh = false) => {
983
+ try {
984
+ return await getTokens(previousAccountId, refresh);
985
+ } catch {
986
+ throw new Error(AUTH_ERROR);
930
987
  }
931
- }
932
- async #ensurePayload() {
933
- const existing = await this.credentials.readRecord(this.key);
934
- if (existing !== void 0) return assertVaultRecord(existing);
935
- const legacy = await this.#legacyCredential();
936
- if (legacy === void 0) return void 0;
937
- const id = this.createId();
938
- return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
939
- if (current !== void 0) return Promise.resolve(current);
940
- return Promise.resolve(grant({
941
- version: VERSION,
942
- activeId: id,
943
- legacyAccountId: id,
944
- accounts: [{
945
- id,
946
- label: DEFAULT_LABEL,
947
- credential: legacy.credential
948
- }]
949
- }));
950
- }));
951
- }
952
- async #modifyPayload(update) {
953
- await this.#ensurePayload();
954
- let previousLegacy;
955
- const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
956
- if (current === void 0) throw new Error("Codex account vault is not signed in");
957
- const payload = assertVaultRecord(current);
958
- previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
959
- const next = await update(clone$1(payload));
960
- return grant(next);
961
- }));
962
- const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
988
+ };
989
+ const login = () => authenticated ??= (async () => {
990
+ const tokens = await authorize();
991
+ signal.throwIfAborted();
963
992
  try {
964
- if (legacy === void 0) {
965
- if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
966
- } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
993
+ return await server.request("account/login/start", {
994
+ type: "chatgptAuthTokens",
995
+ ...tokens
996
+ }, AbortSignal.any([signal, AbortSignal.timeout(1e4)]));
967
997
  } catch {
968
- this.onLegacySyncFailure();
998
+ throw new Error(AUTH_ERROR);
969
999
  }
970
- return payload;
971
- }
972
- list() {
973
- return this.#enqueue(async () => {
974
- const payload = await this.#ensurePayload();
975
- if (payload === void 0) return [];
976
- return payload.accounts.map((account) => ({
977
- id: account.id,
978
- label: account.label,
979
- active: account.id === payload.activeId,
980
- expiresAt: account.credential.expires,
981
- ...account.credential.email === void 0 ? {} : { email: account.credential.email }
982
- }));
983
- });
984
- }
985
- readActive() {
986
- return this.#enqueue(async () => {
987
- const payload = await this.#ensurePayload();
988
- return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
989
- });
990
- }
991
- activeId() {
992
- return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
993
- }
994
- add(label, credential) {
995
- return this.#enqueue(async () => {
996
- const normalizedLabel = normalizeLabel(label);
997
- const validated = sanitizeOAuthCredential(credential);
998
- await this.#ensurePayload();
999
- const id = this.createId();
1000
- const account = (await this.#modifyPayload((current) => ({
1001
- ...current,
1002
- activeId: id,
1003
- accounts: [...current.accounts, {
1004
- id,
1005
- label: normalizedLabel,
1006
- credential: validated
1007
- }]
1008
- }))).accounts.find((candidate) => candidate.id === id);
1009
- return {
1010
- id,
1011
- label: account.label,
1012
- active: true,
1013
- expiresAt: account.credential.expires,
1014
- ...account.credential.email === void 0 ? {} : { email: account.credential.email }
1015
- };
1016
- });
1017
- }
1018
- select(id) {
1019
- return this.#enqueue(async () => {
1020
- await this.#modifyPayload((current) => {
1021
- if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
1022
- return {
1023
- ...current,
1024
- activeId: id
1025
- };
1026
- });
1027
- });
1028
- }
1029
- modifyActive(update) {
1030
- return this.#enqueue(async () => {
1031
- if (await this.#ensurePayload() === void 0) {
1032
- const initial = await update(void 0);
1033
- if (initial === void 0) return void 0;
1034
- const credential = sanitizeOAuthCredential(initial);
1035
- await this.credentials.set(this.legacyRef, JSON.stringify(credential));
1036
- await this.#ensurePayload();
1037
- return clone$1(credential);
1038
- }
1039
- let result;
1040
- await this.#modifyPayload(async (current) => {
1041
- const index = current.accounts.findIndex((account) => account.id === current.activeId);
1042
- const previous = clone$1(current.accounts[index].credential);
1043
- const next = await update(previous);
1044
- if (next === void 0) {
1045
- result = previous;
1046
- return current;
1000
+ })();
1001
+ host.onRequest(async (method, params) => {
1002
+ if (method === "initialize") {
1003
+ const result = await server.request(method, {
1004
+ ...params,
1005
+ capabilities: {
1006
+ ...params.capabilities,
1007
+ experimentalApi: true
1047
1008
  }
1048
- const credential = sanitizeOAuthCredential(next);
1049
- if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
1050
- const accounts = [...current.accounts];
1051
- accounts[index] = {
1052
- ...accounts[index],
1053
- credential
1054
- };
1055
- result = clone$1(credential);
1056
- return {
1057
- ...current,
1058
- accounts
1059
- };
1060
- });
1009
+ }, signal);
1010
+ initialized = true;
1061
1011
  return result;
1062
- });
1063
- }
1064
- deleteAll() {
1065
- return this.#enqueue(async () => {
1066
- await this.credentials.deleteRecord(this.key);
1067
- await this.credentials.unset(this.legacyRef);
1068
- for (const ref of this.legacyRefs) await this.credentials.unset(ref);
1069
- });
1070
- }
1071
- remove(id) {
1072
- return this.#enqueue(async () => {
1073
- await this.#modifyPayload((current) => {
1074
- if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
1075
- if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
1076
- const accounts = current.accounts.filter((account) => account.id !== id);
1077
- return {
1078
- ...current,
1079
- activeId: current.activeId === id ? accounts[0].id : current.activeId,
1080
- legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
1081
- accounts
1082
- };
1083
- });
1084
- });
1085
- }
1086
- };
1087
- //#endregion
1088
- //#region src/credential-store.js
1089
- const PROVIDER$1 = "openai-codex";
1090
- const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
1091
- const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
1092
- function assertProvider(providerId) {
1093
- if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
1012
+ }
1013
+ if (method === "thread/start") {
1014
+ if (!initialized) throw new Error("Codex initialization incomplete");
1015
+ await login();
1016
+ return server.request(method, {
1017
+ ...params,
1018
+ ...thread
1019
+ }, signal);
1020
+ }
1021
+ return server.request(method, params, signal);
1022
+ });
1023
+ host.onNotification((method, params) => server.notify(method, params));
1024
+ server.onRequest((method, params) => method === "account/chatgptAuthTokens/refresh" ? authorize(params.previousAccountId ?? void 0, true) : host.request(method, params, signal));
1025
+ server.onNotification((method, params) => host.notify(method, params));
1026
+ host.start();
1027
+ server.start();
1028
+ child.done.then(close, close);
1029
+ return new Proxy(child, { get(target, key) {
1030
+ if (key === "stdin") return stdin;
1031
+ if (key === "stdout") return stdout;
1032
+ const value = Reflect.get(target, key, target);
1033
+ return typeof value === "function" ? value.bind(target) : value;
1034
+ } });
1094
1035
  }
1095
- function assertOAuthCredential(value) {
1096
- if (value === void 0) return void 0;
1097
- if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex credential store received a malformed OAuth credential");
1098
- return clone(value);
1036
+ //#endregion
1037
+ //#region src/subagent-backend.js
1038
+ const SUBAGENT_PROVIDER = "codex-subscription-subagent";
1039
+ const MODES = /* @__PURE__ */ new Set([
1040
+ "read-only",
1041
+ "workspace-write",
1042
+ "danger-full-access"
1043
+ ]);
1044
+ function subagentThreadPolicy(parent, policy) {
1045
+ if (!MODES.has(policy?.mode)) throw new Error("DSH subagent sandbox policy is unavailable");
1046
+ const selected = parent.session.requestHeader?.()?.config ?? parent.options ?? {};
1047
+ const subscription = selected.provider === "openai-codex" && typeof selected.model === "string";
1048
+ return {
1049
+ model: subscription ? selected.model : "gpt-5.6-luna",
1050
+ modelProvider: "openai",
1051
+ approvalPolicy: "never",
1052
+ sandbox: policy.mode,
1053
+ config: { model_reasoning_effort: subscription ? selected.reasoningEffort ?? "low" : "low" }
1054
+ };
1099
1055
  }
1100
- function parseOAuthCredential(value) {
1101
- try {
1102
- return assertOAuthCredential(JSON.parse(value));
1103
- } catch (error) {
1104
- if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
1105
- throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
1106
- }
1056
+ /** Reuse the official DSH process/turn provider; keep only subscription auth here. */
1057
+ function createSubscriptionSubagent({ ctx, nativeHome, resolveAuth, store, refresh, loadRuntime }) {
1058
+ let runtime;
1059
+ const load = () => runtime ??= loadRuntime().catch((error) => {
1060
+ runtime = void 0;
1061
+ throw error;
1062
+ });
1063
+ const active = /* @__PURE__ */ new Set();
1064
+ let disposed = false;
1065
+ return {
1066
+ provider: {
1067
+ name: SUBAGENT_PROVIDER,
1068
+ capabilities: {
1069
+ agentOptions: false,
1070
+ outputSchema: false,
1071
+ depthLimit: false,
1072
+ toolFilter: false,
1073
+ persona: false
1074
+ },
1075
+ inheritsParentContext: false,
1076
+ async start(request) {
1077
+ if (disposed) throw new Error("Codex subagent is unavailable");
1078
+ const controller = new AbortController();
1079
+ active.add(controller);
1080
+ const signal = AbortSignal.any([request.signal, controller.signal]);
1081
+ let run;
1082
+ try {
1083
+ const thread = subagentThreadPolicy(request.parent, ctx.sandboxPolicy.resolve({ session: request.parent.session }));
1084
+ const getTokens = await createSubagentTokens({
1085
+ resolveAuth,
1086
+ store,
1087
+ refresh,
1088
+ signal
1089
+ });
1090
+ const { official, Transport } = await load();
1091
+ const proxy = await resolveCodexOAuthProxy({ target: new URL("https://chatgpt.com/") });
1092
+ signal.throwIfAborted();
1093
+ await mkdir(nativeHome, { recursive: true });
1094
+ const env = {
1095
+ CODEX_HOME: nativeHome,
1096
+ ...proxy ? {
1097
+ HTTPS_PROXY: proxy,
1098
+ HTTP_PROXY: proxy,
1099
+ ALL_PROXY: proxy
1100
+ } : {}
1101
+ };
1102
+ let delegate;
1103
+ official.apply({
1104
+ subagents: { registerProvider(value) {
1105
+ delegate = value;
1106
+ } },
1107
+ subprocess: { spawn: (spec) => authenticatedSubagentChild(ctx.subprocess.spawn({
1108
+ ...spec,
1109
+ env: {
1110
+ ...spec.env,
1111
+ ...env
1112
+ }
1113
+ }), {
1114
+ Transport,
1115
+ getTokens,
1116
+ thread,
1117
+ signal
1118
+ }) },
1119
+ logger: { warn: () => ctx.logger?.warn?.("Codex subscription subtask failed") }
1120
+ }, {
1121
+ model: thread.model,
1122
+ env,
1123
+ permissionMode: "never",
1124
+ disposeGraceMs: 1e3
1125
+ });
1126
+ run = await delegate.start({
1127
+ ...request,
1128
+ signal
1129
+ });
1130
+ const result = run.result.finally(() => active.delete(controller));
1131
+ return {
1132
+ ...run,
1133
+ result,
1134
+ async dispose() {
1135
+ controller.abort();
1136
+ await run.dispose();
1137
+ active.delete(controller);
1138
+ }
1139
+ };
1140
+ } catch (error) {
1141
+ controller.abort();
1142
+ await run?.dispose();
1143
+ active.delete(controller);
1144
+ throw error;
1145
+ }
1146
+ }
1147
+ },
1148
+ prepare: load,
1149
+ dispose() {
1150
+ disposed = true;
1151
+ for (const controller of active) controller.abort();
1152
+ }
1153
+ };
1107
1154
  }
1108
- /**
1109
- * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
1110
- * Refresh/login/logout operations are serialized so an older refresh response
1111
- * cannot overwrite a newer rotated token.
1112
- */
1113
- var DshOAuthCredentialStore = class {
1114
- #chains = /* @__PURE__ */ new Map();
1115
- constructor(credentials, ref, legacyRefs = [], options = {}) {
1116
- if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
1117
- const expirySkewMs = options.expirySkewMs ?? 0;
1118
- if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
1119
- this.credentials = credentials;
1120
- this.ref = ref;
1121
- this.legacyRefs = Object.freeze([...legacyRefs]);
1122
- this.expirySkewMs = expirySkewMs;
1123
- this.vault = options.vault;
1124
- }
1125
- #enqueue(providerId, operation, options) {
1126
- assertProvider(providerId);
1127
- const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
1128
- abortIfNeeded(options);
1129
- return operation();
1130
- });
1131
- const tail = current.catch(() => void 0);
1132
- this.#chains.set(providerId, tail);
1133
- tail.finally(() => {
1134
- if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
1155
+ /** Change only standard independent spawn tools; leave fork/custom tools untouched. */
1156
+ function createSubagentBackendSwitcher({ entries, prepare, persist }) {
1157
+ const originals = /* @__PURE__ */ new Map();
1158
+ let selected = "dsh";
1159
+ let tail = Promise.resolve();
1160
+ let disposed = false;
1161
+ const standard = (entry, config) => entry?.options?.name === "@deepseek-ai/dsh-tool-subagent" && config?.provider === "spawn" && !config.agentOptions && !config.persona && !config.toolFilter;
1162
+ const convert = (config) => ({
1163
+ ...config,
1164
+ provider: SUBAGENT_PROVIDER,
1165
+ modelSelectionSettings: false,
1166
+ backgroundMode: "one-shot",
1167
+ maxDepth: "provider-managed"
1168
+ });
1169
+ const configure = (fiber, config) => {
1170
+ if (!standard(fiber.entry, config)) return config;
1171
+ if (!originals.has(fiber)) originals.set(fiber, { ...config });
1172
+ return selected === "codex" ? convert(config) : config;
1173
+ };
1174
+ const select = (mode) => {
1175
+ if (!["dsh", "codex"].includes(mode)) return Promise.reject(/* @__PURE__ */ new Error("Invalid subagent backend"));
1176
+ const next = tail.catch(() => {}).then(async () => {
1177
+ if (disposed) throw new Error("Subagent backend is unavailable");
1178
+ const all = [...entries()];
1179
+ for (const entry of all) if (entry.fiber && standard(entry, entry.fiber.config) && !originals.has(entry.fiber)) originals.set(entry.fiber, { ...entry.fiber.config });
1180
+ if (mode === "codex") {
1181
+ if (!originals.size && !all.some((entry) => standard(entry, entry.options?.config))) throw new Error("No standard DSH independent subagent tool is available");
1182
+ await prepare();
1183
+ }
1184
+ const changed = [];
1185
+ const previous = selected;
1186
+ selected = mode;
1187
+ try {
1188
+ for (const [fiber, original] of originals) {
1189
+ if (fiber.entry && fiber.entry.fiber !== fiber) {
1190
+ originals.delete(fiber);
1191
+ continue;
1192
+ }
1193
+ const before = { ...fiber.config };
1194
+ changed.push({
1195
+ fiber,
1196
+ before
1197
+ });
1198
+ await fiber.update(mode === "codex" ? convert(original) : original, true);
1199
+ }
1200
+ await persist?.(mode);
1201
+ } catch (error) {
1202
+ selected = previous;
1203
+ for (const { fiber, before } of changed.reverse()) await fiber.update(before, true);
1204
+ throw error;
1205
+ }
1135
1206
  });
1136
- return current;
1137
- }
1138
- async #read(providerId, options) {
1139
- assertProvider(providerId);
1140
- abortIfNeeded(options);
1141
- if (this.vault !== void 0) {
1142
- const current = await this.vault.readActive();
1143
- if (current === void 0) return void 0;
1144
- return this.expirySkewMs === 0 ? current : {
1145
- ...current,
1146
- expires: current.expires - this.expirySkewMs
1147
- };
1148
- }
1149
- let hit = await this.credentials.resolve(this.ref);
1150
- if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
1151
- const legacy = await this.credentials.resolve(legacyRef);
1152
- if (legacy?.value === void 0 || legacy.value === "") continue;
1153
- const migrated = parseOAuthCredential(legacy.value);
1154
- await this.credentials.set(this.ref, JSON.stringify(migrated));
1155
- await this.credentials.unset(legacyRef);
1156
- hit = { value: JSON.stringify(migrated) };
1157
- break;
1207
+ tail = next;
1208
+ return next;
1209
+ };
1210
+ return {
1211
+ select,
1212
+ configure,
1213
+ async dispose() {
1214
+ await tail.catch(() => {});
1215
+ disposed = true;
1216
+ selected = "dsh";
1217
+ for (const [fiber, original] of originals) if (fiber.config?.provider === "codex-subscription-subagent") await fiber.update(original, true);
1218
+ originals.clear();
1158
1219
  }
1159
- abortIfNeeded(options);
1160
- if (hit?.value === void 0 || hit.value === "") return void 0;
1161
- const credential = parseOAuthCredential(hit.value);
1162
- return this.expirySkewMs === 0 ? credential : {
1163
- ...credential,
1164
- expires: credential.expires - this.expirySkewMs
1165
- };
1166
- }
1167
- read(providerId, options) {
1168
- return this.#enqueue(providerId, () => this.#read(providerId, options), options);
1169
- }
1170
- async list(options) {
1171
- abortIfNeeded(options);
1172
- return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
1173
- providerId: PROVIDER$1,
1174
- type: "oauth"
1175
- }];
1176
- }
1177
- modify(providerId, update, options) {
1178
- return this.#enqueue(providerId, async () => {
1179
- if (this.vault !== void 0) {
1180
- const next = await this.vault.modifyActive(async (current) => {
1181
- const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
1182
- ...current,
1183
- expires: current.expires - this.expirySkewMs
1220
+ };
1221
+ }
1222
+ async function loadSubagentRuntime() {
1223
+ const [official, { JsonRpcLineTransport: Transport }] = await Promise.all([import("@deepseek-ai/dsh-subagent-codex"), import("@deepseek-ai/dsh-sdk-protocol")]);
1224
+ return {
1225
+ official,
1226
+ Transport
1227
+ };
1228
+ }
1229
+ //#endregion
1230
+ //#region src/sketch-agent-bridge.js
1231
+ function createSketchAgentBridge({ enabled, now = Date.now, timeoutMs = 2e4 }) {
1232
+ const sessions = /* @__PURE__ */ new Map();
1233
+ const fail = (entry, message) => {
1234
+ for (const task of entry.tasks.values()) task.reject(Error(message));
1235
+ entry.tasks.clear();
1236
+ };
1237
+ const find = (payload) => {
1238
+ const entry = sessions.get(payload.sessionId);
1239
+ if (!entry || entry.token !== payload.token || now() - entry.seen > 1e4) throw Error("Sketch connection expired");
1240
+ entry.seen = now();
1241
+ return entry;
1242
+ };
1243
+ return {
1244
+ async rpc(endpoint, payload) {
1245
+ try {
1246
+ if (!enabled()) throw Error("Sketch is disabled");
1247
+ if (!payload || typeof payload.sessionId !== "string" || !payload.sessionId.length || payload.sessionId.length > 200) throw Error("Invalid session");
1248
+ if (endpoint === "sketch/connect") {
1249
+ for (const [id, entry] of sessions) if (now() - entry.seen >= 1e4) {
1250
+ fail(entry, "Sketch connection expired");
1251
+ sessions.delete(id);
1252
+ }
1253
+ const previous = sessions.get(payload.sessionId);
1254
+ if (previous && now() - previous.seen < 1e4) throw Error("Another board is connected to this session");
1255
+ if (previous) fail(previous, "Sketch connection replaced");
1256
+ const entry = {
1257
+ token: randomUUID(),
1258
+ seen: now(),
1259
+ tasks: /* @__PURE__ */ new Map(),
1260
+ cancelled: []
1184
1261
  };
1185
- const updated = await update(clone(visible));
1186
- return updated === void 0 ? void 0 : assertOAuthCredential(updated);
1187
- });
1188
- abortIfNeeded(options);
1189
- return clone(next);
1262
+ sessions.set(payload.sessionId, entry);
1263
+ return {
1264
+ ok: true,
1265
+ value: { token: entry.token }
1266
+ };
1267
+ }
1268
+ const entry = find(payload);
1269
+ if (endpoint === "sketch/poll") return {
1270
+ ok: true,
1271
+ value: [...entry.cancelled.splice(0).map((id) => ({
1272
+ id,
1273
+ cancelled: true
1274
+ })), ...[...entry.tasks].filter(([, t]) => !t.delivered).map(([id, t]) => {
1275
+ t.delivered = true;
1276
+ return {
1277
+ id,
1278
+ request: t.request,
1279
+ expiresAt: t.expiresAt
1280
+ };
1281
+ })]
1282
+ };
1283
+ if (endpoint === "sketch/claim") {
1284
+ const task = entry.tasks.get(payload.id);
1285
+ return {
1286
+ ok: true,
1287
+ value: Boolean(task && task.delivered && task.expiresAt > now())
1288
+ };
1289
+ }
1290
+ if (endpoint === "sketch/disconnect") {
1291
+ fail(entry, "Sketch board closed");
1292
+ sessions.delete(payload.sessionId);
1293
+ return {
1294
+ ok: true,
1295
+ value: null
1296
+ };
1297
+ }
1298
+ if (endpoint === "sketch/result") {
1299
+ const task = entry.tasks.get(payload.id);
1300
+ if (task) {
1301
+ entry.tasks.delete(payload.id);
1302
+ payload.error ? task.reject(Error(String(payload.error).slice(0, 500))) : task.resolve(payload.value);
1303
+ }
1304
+ return {
1305
+ ok: true,
1306
+ value: null
1307
+ };
1308
+ }
1309
+ throw Error("Unknown sketch route");
1310
+ } catch (error) {
1311
+ return {
1312
+ ok: false,
1313
+ error: {
1314
+ code: "invalid-input",
1315
+ message: error.message,
1316
+ details: { issues: [] }
1317
+ }
1318
+ };
1190
1319
  }
1191
- const current = await this.#read(providerId, options);
1192
- const next = await update(clone(current));
1193
- abortIfNeeded(options);
1194
- if (next === void 0) return current;
1195
- const validated = assertOAuthCredential(next);
1196
- await this.credentials.set(this.ref, JSON.stringify(validated));
1197
- for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
1198
- abortIfNeeded(options);
1199
- return clone(validated);
1200
- }, options);
1320
+ },
1321
+ request(sessionId, request, signal) {
1322
+ if (!enabled()) return Promise.reject(Error("Sketch is disabled"));
1323
+ const entry = sessions.get(sessionId);
1324
+ if (!entry || now() - entry.seen > 1e4) return Promise.reject(Error("Switch to this session in DSH with sketch editing enabled"));
1325
+ if (entry.tasks.size) return Promise.reject(Error("Another sketch operation is pending"));
1326
+ if (JSON.stringify(request).length > 2e6) return Promise.reject(Error("Sketch batch is too large"));
1327
+ return new Promise((resolve, reject) => {
1328
+ const id = randomUUID();
1329
+ const finish = (callback, value) => {
1330
+ clearTimeout(timer);
1331
+ signal?.removeEventListener("abort", abort);
1332
+ entry.tasks.delete(id);
1333
+ callback(value);
1334
+ };
1335
+ const cancel = (message) => {
1336
+ if (entry.tasks.get(id)?.delivered) {
1337
+ entry.cancelled.push(id);
1338
+ if (entry.cancelled.length > 32) entry.cancelled.shift();
1339
+ }
1340
+ finish(reject, Error(message));
1341
+ };
1342
+ const abort = () => cancel("Sketch operation interrupted; inspect recentRequests before retrying");
1343
+ const timer = setTimeout(() => cancel("Sketch response timed out; inspect recentRequests before retrying"), timeoutMs);
1344
+ entry.tasks.set(id, {
1345
+ request,
1346
+ expiresAt: now() + timeoutMs,
1347
+ delivered: false,
1348
+ resolve: (value) => finish(resolve, value),
1349
+ reject: (error) => finish(reject, error)
1350
+ });
1351
+ signal?.addEventListener("abort", abort, { once: true });
1352
+ if (signal?.aborted) abort();
1353
+ });
1354
+ },
1355
+ dispose() {
1356
+ for (const entry of sessions.values()) fail(entry, "Sketch service stopped");
1357
+ sessions.clear();
1358
+ }
1359
+ };
1360
+ }
1361
+ //#endregion
1362
+ //#region src/sketch-command-schema.js
1363
+ const number = { type: "number" };
1364
+ const string = { type: "string" };
1365
+ const object = (properties) => ({
1366
+ type: "object",
1367
+ additionalProperties: false,
1368
+ properties
1369
+ });
1370
+ const points = {
1371
+ type: "array",
1372
+ items: object({
1373
+ x: {
1374
+ ...number,
1375
+ required: true
1376
+ },
1377
+ y: {
1378
+ ...number,
1379
+ required: true
1380
+ }
1381
+ })
1382
+ };
1383
+ const point = object({
1384
+ x: {
1385
+ ...number,
1386
+ required: true
1387
+ },
1388
+ y: {
1389
+ ...number,
1390
+ required: true
1201
1391
  }
1202
- delete(providerId, options) {
1203
- return this.#enqueue(providerId, async () => {
1204
- if (this.vault !== void 0) {
1205
- await this.vault.deleteAll();
1206
- abortIfNeeded(options);
1207
- return;
1392
+ });
1393
+ const style = {
1394
+ color: string,
1395
+ width: number,
1396
+ opacity: number,
1397
+ fill: { type: "boolean" },
1398
+ text: string,
1399
+ points
1400
+ };
1401
+ const sketchCommandArray = {
1402
+ type: "array",
1403
+ items: object({
1404
+ op: {
1405
+ type: "string",
1406
+ required: true,
1407
+ enum: [
1408
+ "stroke",
1409
+ "object",
1410
+ "layer",
1411
+ "resize"
1412
+ ]
1413
+ },
1414
+ id: {
1415
+ oneOf: [{ type: "string" }, { type: "integer" }],
1416
+ description: "Object string ID. Layer add: optional NEW unique integer ID; other layer actions: existing layer ID."
1417
+ },
1418
+ after: {
1419
+ type: "integer",
1420
+ description: "Layer add only: existing layer to insert after; defaults to active."
1421
+ },
1422
+ start: point,
1423
+ segments: {
1424
+ type: "array",
1425
+ items: object({
1426
+ control1: {
1427
+ ...point,
1428
+ required: true
1429
+ },
1430
+ control2: {
1431
+ ...point,
1432
+ required: true
1433
+ },
1434
+ end: {
1435
+ ...point,
1436
+ required: true
1437
+ }
1438
+ })
1439
+ },
1440
+ layer: { type: "integer" },
1441
+ shape: {
1442
+ type: "string",
1443
+ enum: [
1444
+ "pen",
1445
+ "line",
1446
+ "arrow",
1447
+ "text",
1448
+ "rectangle",
1449
+ "circle",
1450
+ "ellipse",
1451
+ "polygon",
1452
+ "bezier",
1453
+ "eraser",
1454
+ "triangle",
1455
+ "diamond",
1456
+ "star"
1457
+ ]
1458
+ },
1459
+ ...style,
1460
+ action: {
1461
+ type: "string",
1462
+ enum: [
1463
+ "update",
1464
+ "duplicate",
1465
+ "delete",
1466
+ "add",
1467
+ "select",
1468
+ "rename",
1469
+ "visible",
1470
+ "up",
1471
+ "down",
1472
+ "clear"
1473
+ ]
1474
+ },
1475
+ value: string,
1476
+ ratio: {
1477
+ type: "string",
1478
+ enum: [
1479
+ "1:1",
1480
+ "4:3",
1481
+ "3:4",
1482
+ "16:9",
1483
+ "9:16"
1484
+ ]
1485
+ },
1486
+ patch: object(style),
1487
+ transform: object({
1488
+ dx: number,
1489
+ dy: number,
1490
+ scaleX: number,
1491
+ scaleY: number
1492
+ })
1493
+ })
1494
+ };
1495
+ //#endregion
1496
+ //#region src/sketch-agent-tool.js
1497
+ function createSketchAgentTool(bridge, attachments) {
1498
+ return defineTool({
1499
+ name: "codex_sketch",
1500
+ description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
1501
+ parameters: {
1502
+ action: {
1503
+ type: "string",
1504
+ required: true,
1505
+ enum: [
1506
+ "inspect",
1507
+ "apply",
1508
+ "preview",
1509
+ "save",
1510
+ "finish"
1511
+ ]
1512
+ },
1513
+ runId: {
1514
+ type: "string",
1515
+ description: "From inspect; required for all other actions. Never reuse a stopped run."
1516
+ },
1517
+ documentId: {
1518
+ type: "string",
1519
+ description: "From inspect; required except for inspect."
1520
+ },
1521
+ revision: {
1522
+ type: "integer",
1523
+ description: "From latest response; required for apply/save/finish."
1524
+ },
1525
+ requestId: {
1526
+ type: "string",
1527
+ description: "Unique id for apply/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
1528
+ },
1529
+ commands: {
1530
+ oneOf: [sketchCommandArray, { type: "string" }],
1531
+ description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
1532
+ },
1533
+ name: {
1534
+ type: "string",
1535
+ description: "Draft name for save/finish."
1536
+ },
1537
+ offset: {
1538
+ type: "integer",
1539
+ description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
1540
+ },
1541
+ objectId: {
1542
+ type: "string",
1543
+ description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
1544
+ },
1545
+ layer: {
1546
+ type: "integer",
1547
+ description: "inspect only: layer containing objectId."
1208
1548
  }
1209
- await this.credentials.unset(this.ref);
1210
- for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
1211
- abortIfNeeded(options);
1212
- }, options);
1213
- }
1214
- };
1215
- /** Return only account state that is safe to expose to the browser client. */
1216
- function createCodexAuthService(models, store, options = {}) {
1217
- const runLogin = options.runLogin ?? ((run) => run());
1218
- const accountVault = options.accountVault;
1219
- const createLoginModels = options.createLoginModels;
1220
- const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
1221
- return Object.freeze({
1222
- async status(options) {
1223
- const current = await store.read(PROVIDER$1, options);
1224
- const accounts = await accountVault?.list();
1225
- if (current === void 0) return {
1226
- authenticated: false,
1227
- provider: PROVIDER$1,
1228
- ...accounts === void 0 ? {} : { accounts }
1229
- };
1230
- return {
1231
- authenticated: true,
1232
- provider: PROVIDER$1,
1233
- type: "oauth",
1234
- expiresAt: current.expires,
1235
- ...accounts === void 0 ? {} : { accounts }
1236
- };
1237
1549
  },
1238
- login(interaction, input = {}) {
1239
- if (input.label !== void 0) {
1240
- if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
1241
- return runLogin(async () => {
1242
- const pending = createPendingStore();
1243
- await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
1244
- const credential = pending.credential();
1245
- if (credential === void 0) throw new Error("Codex login did not return credentials");
1246
- await accountVault.add(input.label, credential);
1550
+ timeoutMs: 25e3,
1551
+ isConcurrencySafe: () => false,
1552
+ async execute(args, exec) {
1553
+ const sessionId = exec.agent?.id;
1554
+ if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
1555
+ const request = { ...args };
1556
+ if (args.action === "apply") try {
1557
+ request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
1558
+ if (!Array.isArray(request.commands)) throw Error();
1559
+ } catch {
1560
+ throw Error("commands must be a native array or JSON array string");
1561
+ }
1562
+ const value = await bridge.request(sessionId, request, exec.signal);
1563
+ if (value.png) {
1564
+ if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
1565
+ const image = await attachments.saveImage({
1566
+ data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
1567
+ mediaType: "image/png",
1568
+ name: "sketch-preview.png"
1247
1569
  });
1570
+ const { png, ...snapshot } = value;
1571
+ return {
1572
+ ...snapshot,
1573
+ image
1574
+ };
1248
1575
  }
1249
- return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
1250
- },
1251
- async select(id) {
1252
- if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
1253
- await accountVault.select(id);
1254
- return this.status();
1255
- },
1256
- async remove(id) {
1257
- if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
1258
- await accountVault.remove(id);
1259
- return this.status();
1576
+ return value;
1260
1577
  },
1261
- logout(options) {
1262
- return models.logout(PROVIDER$1, options);
1578
+ output: {
1579
+ schema: {
1580
+ type: "object",
1581
+ additionalProperties: true
1582
+ },
1583
+ render: (_args, value) => [{
1584
+ type: "text",
1585
+ text: JSON.stringify({
1586
+ ...value,
1587
+ image: void 0
1588
+ })
1589
+ }, ...value.image ? [{
1590
+ type: "image",
1591
+ attachment: value.image
1592
+ }] : []]
1263
1593
  }
1264
1594
  });
1265
1595
  }
1266
1596
  //#endregion
1267
- //#region src/external-url.js
1268
- const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
1269
- /** Validate the only external origin this plugin may launch. */
1270
- function assertCodexAuthUrl(value) {
1271
- let url;
1597
+ //#region src/sketch-codec-route.js
1598
+ function registerSketchCodec(connection) {
1599
+ let source;
1600
+ return connection.fetch.register({
1601
+ path: "/api/codex-subscription/sketch-psd-worker",
1602
+ methods: ["GET"],
1603
+ requestBody: "buffered",
1604
+ async fetch() {
1605
+ source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
1606
+ return new Response(source, { headers: {
1607
+ "content-type": "text/javascript; charset=utf-8",
1608
+ "cache-control": "no-store"
1609
+ } });
1610
+ }
1611
+ });
1612
+ }
1613
+ //#endregion
1614
+ //#region src/account-vault.js
1615
+ const VERSION = 1;
1616
+ const DEFAULT_LABEL = "Account 1";
1617
+ const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
1618
+ const EMAIL_MAX_LENGTH = 254;
1619
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
1620
+ /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
1621
+ function normalizeAccountEmail(value) {
1622
+ if (typeof value !== "string") return void 0;
1623
+ const email = value.trim();
1624
+ return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
1625
+ }
1626
+ function decodeJwtPayload(access) {
1627
+ if (typeof access !== "string") return void 0;
1628
+ const encoded = access.split(".")[1];
1629
+ if (typeof encoded !== "string" || encoded.length === 0) return void 0;
1272
1630
  try {
1273
- url = new URL(value);
1631
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
1274
1632
  } catch {
1275
- throw new Error("Codex auth URL is invalid");
1633
+ return;
1276
1634
  }
1277
- if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
1278
- if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
1279
- return url.href;
1280
1635
  }
1281
- /** Return a shell-free native opener command for the current desktop. */
1282
- function commandForCodexAuthUrl(value, platform = process.platform) {
1283
- const url = assertCodexAuthUrl(value);
1284
- if (platform === "win32") return {
1285
- file: "rundll32.exe",
1286
- args: ["url.dll,FileProtocolHandler", url],
1287
- shell: false
1288
- };
1289
- if (platform === "darwin") return {
1290
- file: "open",
1291
- args: [url],
1292
- shell: false
1293
- };
1294
- if (platform === "linux") return {
1295
- file: "xdg-open",
1296
- args: [url],
1297
- shell: false
1298
- };
1299
- throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
1300
- }
1301
- function openCodexAuthUrl(value, options = {}) {
1302
- const command = commandForCodexAuthUrl(value, options.platform);
1303
- const spawnProcess = options.spawn ?? spawn;
1304
- return new Promise((resolve, reject) => {
1305
- const child = spawnProcess(command.file, command.args, {
1306
- detached: true,
1307
- stdio: "ignore",
1308
- windowsHide: true,
1309
- shell: command.shell
1310
- });
1311
- child.once("error", reject);
1312
- child.once("spawn", () => {
1313
- child.unref();
1314
- resolve();
1315
- });
1316
- });
1636
+ function emailFromAccessToken(access) {
1637
+ const payload = decodeJwtPayload(access);
1638
+ return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
1317
1639
  }
1318
- //#endregion
1319
- //#region src/login-coordinator.js
1320
- const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
1321
- const TERMINAL_PHASES = /* @__PURE__ */ new Set([
1322
- "authenticated",
1323
- "failed",
1324
- "cancelled"
1325
- ]);
1326
- const publicClone = (value) => structuredClone(value);
1327
- const asObject = (value) => value !== null && typeof value === "object" ? value : {};
1328
- const ok = (value) => ({
1329
- ok: true,
1330
- value
1331
- });
1332
- const badRequest = (message) => ({
1333
- ok: false,
1334
- error: {
1335
- code: "bad-request",
1336
- message,
1337
- details: { issues: [] }
1338
- }
1339
- });
1340
- const accountStatusError = (message) => ({
1341
- ok: false,
1342
- error: {
1343
- code: "internal",
1344
- message,
1345
- details: { issues: [] }
1346
- }
1347
- });
1348
- const classifyAccountStatusError = (error) => {
1349
- const message = error instanceof Error ? error.message : "";
1350
- if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
1351
- if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
1352
- const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
1353
- if (error?.name === "TimeoutError" || [
1354
- "TIMEOUT",
1355
- "ETIMEDOUT",
1356
- "UND_ERR_CONNECT_TIMEOUT"
1357
- ].includes(code)) return ["transport", "Codex account status service is unavailable"];
1358
- if ([
1359
- "ECONNRESET",
1360
- "ECONNREFUSED",
1361
- "ENOTFOUND",
1362
- "EAI_AGAIN",
1363
- "NETWORK",
1364
- "NETWORK_ERROR",
1365
- "TRANSPORT"
1366
- ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
1367
- return ["unknown", "Could not read Codex account status"];
1368
- };
1369
- const deferred = () => {
1370
- let resolve;
1371
- let reject;
1640
+ /** Normalize the one non-secret account attribute that may cross the UI boundary. */
1641
+ function sanitizeOAuthCredential(value) {
1642
+ const credential = assertOAuthCredential$1(value);
1643
+ const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
1644
+ if (email === void 0) {
1645
+ delete credential.email;
1646
+ return credential;
1647
+ }
1372
1648
  return {
1373
- promise: new Promise((onResolve, onReject) => {
1374
- resolve = onResolve;
1375
- reject = onReject;
1376
- }),
1377
- resolve,
1378
- reject
1649
+ ...credential,
1650
+ email
1379
1651
  };
1380
- };
1381
- const publicPrompt = (prompt) => ({
1382
- type: prompt.type,
1383
- message: String(prompt.message ?? ""),
1384
- ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
1385
- });
1386
- function classifyLoginFailure(error) {
1387
- const message = error instanceof Error ? error.message : "";
1388
- if (/token exchange failed/iu.test(message)) return "token-exchange";
1389
- if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
1390
- if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
1391
- if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
1392
- if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
1393
- return "provider";
1394
1652
  }
1395
- /** Own one host-side login without exposing tokens to the browser client. */
1396
- var CodexLoginCoordinator = class {
1397
- #sessions = /* @__PURE__ */ new Map();
1398
- #activeId;
1399
- constructor(auth, options = {}) {
1400
- this.auth = auth;
1401
- this.createId = options.createId ?? (() => crypto.randomUUID());
1402
- }
1403
- async accountStatus(options) {
1404
- return publicClone(await this.auth.status(options));
1653
+ function assertOAuthCredential$1(value) {
1654
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
1655
+ return clone$1(value);
1656
+ }
1657
+ function parseOAuthCredential$1(value) {
1658
+ try {
1659
+ return assertOAuthCredential$1(JSON.parse(value));
1660
+ } catch (error) {
1661
+ if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
1662
+ throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
1405
1663
  }
1406
- supportState() {
1407
- const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
1408
- if (active === void 0) return { phase: "idle" };
1664
+ }
1665
+ function normalizeLabel(value) {
1666
+ if (typeof value !== "string") throw new Error("Codex account label must be text");
1667
+ const label = value.trim().replace(/\s+/gu, " ");
1668
+ if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
1669
+ return label;
1670
+ }
1671
+ function assertVaultRecord(record) {
1672
+ if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
1673
+ const ids = /* @__PURE__ */ new Set();
1674
+ const accounts = record.payload.accounts.map((account) => {
1675
+ if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
1676
+ ids.add(account.id);
1409
1677
  return {
1410
- method: active.view.method,
1411
- phase: active.view.phase,
1412
- ...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
1678
+ id: account.id,
1679
+ label: normalizeLabel(account.label),
1680
+ credential: sanitizeOAuthCredential(account.credential)
1413
1681
  };
1682
+ });
1683
+ if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
1684
+ const legacyAccountId = record.payload.legacyAccountId;
1685
+ if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
1686
+ return {
1687
+ version: VERSION,
1688
+ activeId: record.payload.activeId,
1689
+ legacyAccountId,
1690
+ accounts
1691
+ };
1692
+ }
1693
+ const grant = (payload) => ({
1694
+ kind: "grant",
1695
+ payload
1696
+ });
1697
+ var PendingOAuthCredentialStore = class {
1698
+ #credential;
1699
+ async read(providerId) {
1700
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
1701
+ return clone$1(this.#credential);
1414
1702
  }
1415
- async start({ method, label }) {
1416
- if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
1417
- if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
1418
- const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
1419
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
1420
- active.view = {
1421
- id: active.view.id,
1422
- provider: "openai-codex",
1423
- method: active.view.method,
1424
- phase: "cancelled",
1425
- authenticated: false
1703
+ async list() {
1704
+ return this.#credential === void 0 ? [] : [{
1705
+ providerId: "openai-codex",
1706
+ type: "oauth"
1707
+ }];
1708
+ }
1709
+ async modify(providerId, update) {
1710
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
1711
+ const next = await update(clone$1(this.#credential));
1712
+ if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
1713
+ return clone$1(this.#credential);
1714
+ }
1715
+ async delete(providerId) {
1716
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
1717
+ this.#credential = void 0;
1718
+ }
1719
+ credential() {
1720
+ return clone$1(this.#credential);
1721
+ }
1722
+ };
1723
+ /**
1724
+ * Multi-account owner state stored in DSH's atomic plugin credential record.
1725
+ * The old single-account reference remains as a rollback source and is kept in
1726
+ * sync whenever that imported account rotates its refresh token.
1727
+ */
1728
+ var DshOAuthAccountVault = class {
1729
+ #tail = Promise.resolve();
1730
+ constructor(credentials, options) {
1731
+ if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
1732
+ this.credentials = credentials;
1733
+ this.key = options.key;
1734
+ this.legacyRef = options.legacyRef;
1735
+ this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
1736
+ this.createId = options.createId ?? randomUUID;
1737
+ this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
1738
+ }
1739
+ #enqueue(operation) {
1740
+ const current = this.#tail.catch(() => void 0).then(operation);
1741
+ this.#tail = current.catch(() => void 0);
1742
+ return current;
1743
+ }
1744
+ async #legacyCredential() {
1745
+ for (const ref of [this.legacyRef, ...this.legacyRefs]) {
1746
+ const hit = await this.credentials.resolve(ref);
1747
+ if (hit?.value === void 0 || hit.value === "") continue;
1748
+ return {
1749
+ ref,
1750
+ credential: parseOAuthCredential$1(hit.value)
1426
1751
  };
1427
- active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
1428
1752
  }
1429
- if (active !== void 0) this.#sessions.delete(active.view.id);
1753
+ }
1754
+ async #ensurePayload() {
1755
+ const existing = await this.credentials.readRecord(this.key);
1756
+ if (existing !== void 0) return assertVaultRecord(existing);
1757
+ const legacy = await this.#legacyCredential();
1758
+ if (legacy === void 0) return void 0;
1430
1759
  const id = this.createId();
1431
- const ready = deferred();
1432
- const controller = new AbortController();
1433
- const session = {
1434
- controller,
1435
- prompt: void 0,
1436
- ready,
1437
- view: {
1438
- id,
1439
- provider: "openai-codex",
1440
- method,
1441
- phase: "starting",
1442
- authenticated: false
1443
- }
1444
- };
1445
- this.#sessions.set(id, session);
1446
- this.#activeId = id;
1447
- const publishReady = () => ready.resolve(publicClone(session.view));
1448
- const interaction = {
1449
- signal: controller.signal,
1450
- prompt: async (prompt) => {
1451
- controller.signal.throwIfAborted();
1452
- if (prompt.type === "select") return method;
1453
- if (![
1454
- "manual_code",
1455
- "text",
1456
- "secret"
1457
- ].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
1458
- const answer = deferred();
1459
- session.prompt = answer;
1460
- session.view = {
1461
- ...session.view,
1462
- phase: "waiting_input",
1463
- prompt: publicPrompt(prompt)
1464
- };
1465
- const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
1466
- controller.signal.addEventListener("abort", abortPrompt, { once: true });
1467
- prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
1468
- publishReady();
1469
- try {
1470
- return await answer.promise;
1471
- } finally {
1472
- controller.signal.removeEventListener("abort", abortPrompt);
1473
- prompt.signal?.removeEventListener("abort", abortPrompt);
1474
- if (session.prompt === answer) session.prompt = void 0;
1475
- }
1476
- },
1477
- notify: (event) => {
1478
- if (controller.signal.aborted) return;
1479
- if (event.type === "auth_url") session.view = {
1480
- ...session.view,
1481
- phase: "waiting_browser",
1482
- authUrl: assertCodexAuthUrl(event.url),
1483
- ...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
1484
- };
1485
- else if (event.type === "device_code") session.view = {
1486
- ...session.view,
1487
- phase: "waiting_device",
1488
- deviceCode: {
1489
- userCode: event.userCode,
1490
- verificationUri: assertCodexAuthUrl(event.verificationUri),
1491
- ...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
1492
- ...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
1493
- }
1494
- };
1495
- else session.view = {
1496
- ...session.view,
1497
- message: String(event.message ?? "")
1498
- };
1499
- publishReady();
1500
- }
1501
- };
1502
- session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
1503
- if (controller.signal.aborted) return;
1504
- const status = await this.auth.status();
1505
- session.view = {
1760
+ return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
1761
+ if (current !== void 0) return Promise.resolve(current);
1762
+ return Promise.resolve(grant({
1763
+ version: VERSION,
1764
+ activeId: id,
1765
+ legacyAccountId: id,
1766
+ accounts: [{
1767
+ id,
1768
+ label: DEFAULT_LABEL,
1769
+ credential: legacy.credential
1770
+ }]
1771
+ }));
1772
+ }));
1773
+ }
1774
+ async #modifyPayload(update) {
1775
+ await this.#ensurePayload();
1776
+ let previousLegacy;
1777
+ const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
1778
+ if (current === void 0) throw new Error("Codex account vault is not signed in");
1779
+ const payload = assertVaultRecord(current);
1780
+ previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
1781
+ const next = await update(clone$1(payload));
1782
+ return grant(next);
1783
+ }));
1784
+ const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
1785
+ try {
1786
+ if (legacy === void 0) {
1787
+ if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
1788
+ } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
1789
+ } catch {
1790
+ this.onLegacySyncFailure();
1791
+ }
1792
+ return payload;
1793
+ }
1794
+ list() {
1795
+ return this.#enqueue(async () => {
1796
+ const payload = await this.#ensurePayload();
1797
+ if (payload === void 0) return [];
1798
+ return payload.accounts.map((account) => ({
1799
+ id: account.id,
1800
+ label: account.label,
1801
+ active: account.id === payload.activeId,
1802
+ expiresAt: account.credential.expires,
1803
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
1804
+ }));
1805
+ });
1806
+ }
1807
+ readActive() {
1808
+ return this.#enqueue(async () => {
1809
+ const payload = await this.#ensurePayload();
1810
+ return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
1811
+ });
1812
+ }
1813
+ activeId() {
1814
+ return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
1815
+ }
1816
+ add(label, credential) {
1817
+ return this.#enqueue(async () => {
1818
+ const normalizedLabel = normalizeLabel(label);
1819
+ const validated = sanitizeOAuthCredential(credential);
1820
+ await this.#ensurePayload();
1821
+ const id = this.createId();
1822
+ const account = (await this.#modifyPayload((current) => ({
1823
+ ...current,
1824
+ activeId: id,
1825
+ accounts: [...current.accounts, {
1826
+ id,
1827
+ label: normalizedLabel,
1828
+ credential: validated
1829
+ }]
1830
+ }))).accounts.find((candidate) => candidate.id === id);
1831
+ return {
1506
1832
  id,
1507
- provider: "openai-codex",
1508
- method,
1509
- phase: "authenticated",
1510
- authenticated: status.authenticated === true,
1511
- ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
1833
+ label: account.label,
1834
+ active: true,
1835
+ expiresAt: account.credential.expires,
1836
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
1512
1837
  };
1513
- }).catch(async (error) => {
1514
- if (controller.signal.aborted) {
1515
- session.view = {
1516
- id,
1517
- provider: "openai-codex",
1518
- method,
1519
- phase: "cancelled",
1520
- authenticated: false
1838
+ });
1839
+ }
1840
+ select(id) {
1841
+ return this.#enqueue(async () => {
1842
+ await this.#modifyPayload((current) => {
1843
+ if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
1844
+ return {
1845
+ ...current,
1846
+ activeId: id
1521
1847
  };
1522
- return;
1848
+ });
1849
+ });
1850
+ }
1851
+ modifyActive(update) {
1852
+ return this.#enqueue(async () => {
1853
+ if (await this.#ensurePayload() === void 0) {
1854
+ const initial = await update(void 0);
1855
+ if (initial === void 0) return void 0;
1856
+ const credential = sanitizeOAuthCredential(initial);
1857
+ await this.credentials.set(this.legacyRef, JSON.stringify(credential));
1858
+ await this.#ensurePayload();
1859
+ return clone$1(credential);
1523
1860
  }
1524
- try {
1525
- if (label !== void 0) throw error;
1526
- const status = await this.auth.status();
1527
- if (status.authenticated === true) {
1528
- session.view = {
1529
- id,
1530
- provider: "openai-codex",
1531
- method,
1532
- phase: "authenticated",
1533
- authenticated: true,
1534
- ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
1535
- };
1536
- return;
1861
+ let result;
1862
+ await this.#modifyPayload(async (current) => {
1863
+ const index = current.accounts.findIndex((account) => account.id === current.activeId);
1864
+ const previous = clone$1(current.accounts[index].credential);
1865
+ const next = await update(previous);
1866
+ if (next === void 0) {
1867
+ result = previous;
1868
+ return current;
1537
1869
  }
1538
- } catch {}
1539
- session.view = {
1540
- id,
1541
- provider: "openai-codex",
1542
- method,
1543
- phase: "failed",
1544
- authenticated: false,
1545
- error: "Codex login failed"
1546
- };
1547
- session.hostError = error;
1548
- }).finally(publishReady);
1549
- return ready.promise;
1870
+ const credential = sanitizeOAuthCredential(next);
1871
+ if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
1872
+ const accounts = [...current.accounts];
1873
+ accounts[index] = {
1874
+ ...accounts[index],
1875
+ credential
1876
+ };
1877
+ result = clone$1(credential);
1878
+ return {
1879
+ ...current,
1880
+ accounts
1881
+ };
1882
+ });
1883
+ return result;
1884
+ });
1550
1885
  }
1551
- read(id) {
1552
- const session = this.#sessions.get(id);
1553
- if (session === void 0) throw new Error("unknown Codex login");
1554
- return publicClone(session.view);
1886
+ deleteAll() {
1887
+ return this.#enqueue(async () => {
1888
+ await this.credentials.deleteRecord(this.key);
1889
+ await this.credentials.unset(this.legacyRef);
1890
+ for (const ref of this.legacyRefs) await this.credentials.unset(ref);
1891
+ });
1555
1892
  }
1556
- async submit({ id, value }) {
1557
- const session = this.#sessions.get(id);
1558
- if (session === void 0) throw new Error("unknown Codex login");
1559
- if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
1560
- if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
1561
- const answer = session.prompt;
1562
- session.prompt = void 0;
1563
- session.view = {
1564
- ...session.view,
1565
- phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
1566
- prompt: void 0
1567
- };
1568
- answer.resolve(value);
1569
- return this.read(id);
1893
+ remove(id) {
1894
+ return this.#enqueue(async () => {
1895
+ await this.#modifyPayload((current) => {
1896
+ if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
1897
+ if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
1898
+ const accounts = current.accounts.filter((account) => account.id !== id);
1899
+ return {
1900
+ ...current,
1901
+ activeId: current.activeId === id ? accounts[0].id : current.activeId,
1902
+ legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
1903
+ accounts
1904
+ };
1905
+ });
1906
+ });
1570
1907
  }
1571
- async cancel(id) {
1572
- const session = this.#sessions.get(id);
1573
- if (session === void 0) throw new Error("unknown Codex login");
1574
- if (!TERMINAL_PHASES.has(session.view.phase)) {
1575
- session.view = {
1576
- id,
1577
- provider: "openai-codex",
1578
- method: session.view.method,
1579
- phase: "cancelled",
1580
- authenticated: false
1908
+ };
1909
+ //#endregion
1910
+ //#region src/credential-store.js
1911
+ const PROVIDER$1 = "openai-codex";
1912
+ const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
1913
+ const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
1914
+ function assertProvider(providerId) {
1915
+ if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
1916
+ }
1917
+ function assertOAuthCredential(value) {
1918
+ if (value === void 0) return void 0;
1919
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex credential store received a malformed OAuth credential");
1920
+ return clone(value);
1921
+ }
1922
+ function parseOAuthCredential(value) {
1923
+ try {
1924
+ return assertOAuthCredential(JSON.parse(value));
1925
+ } catch (error) {
1926
+ if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
1927
+ throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
1928
+ }
1929
+ }
1930
+ /**
1931
+ * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
1932
+ * Refresh/login/logout operations are serialized so an older refresh response
1933
+ * cannot overwrite a newer rotated token.
1934
+ */
1935
+ var DshOAuthCredentialStore = class {
1936
+ #chains = /* @__PURE__ */ new Map();
1937
+ constructor(credentials, ref, legacyRefs = [], options = {}) {
1938
+ if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
1939
+ const expirySkewMs = options.expirySkewMs ?? 0;
1940
+ if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
1941
+ this.credentials = credentials;
1942
+ this.ref = ref;
1943
+ this.legacyRefs = Object.freeze([...legacyRefs]);
1944
+ this.expirySkewMs = expirySkewMs;
1945
+ this.vault = options.vault;
1946
+ }
1947
+ #enqueue(providerId, operation, options) {
1948
+ assertProvider(providerId);
1949
+ const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
1950
+ abortIfNeeded(options);
1951
+ return operation();
1952
+ });
1953
+ const tail = current.catch(() => void 0);
1954
+ this.#chains.set(providerId, tail);
1955
+ tail.finally(() => {
1956
+ if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
1957
+ });
1958
+ return current;
1959
+ }
1960
+ async #read(providerId, options) {
1961
+ assertProvider(providerId);
1962
+ abortIfNeeded(options);
1963
+ if (this.vault !== void 0) {
1964
+ const current = await this.vault.readActive();
1965
+ if (current === void 0) return void 0;
1966
+ return this.expirySkewMs === 0 ? current : {
1967
+ ...current,
1968
+ expires: current.expires - this.expirySkewMs
1581
1969
  };
1582
- session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
1583
1970
  }
1584
- return this.read(id);
1585
- }
1586
- async logout(options) {
1587
- if (this.#activeId !== void 0) {
1588
- const active = this.#sessions.get(this.#activeId);
1589
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
1971
+ let hit = await this.credentials.resolve(this.ref);
1972
+ if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
1973
+ const legacy = await this.credentials.resolve(legacyRef);
1974
+ if (legacy?.value === void 0 || legacy.value === "") continue;
1975
+ const migrated = parseOAuthCredential(legacy.value);
1976
+ await this.credentials.set(this.ref, JSON.stringify(migrated));
1977
+ await this.credentials.unset(legacyRef);
1978
+ hit = { value: JSON.stringify(migrated) };
1979
+ break;
1590
1980
  }
1591
- await this.auth.logout(options);
1592
- return this.accountStatus(options);
1981
+ abortIfNeeded(options);
1982
+ if (hit?.value === void 0 || hit.value === "") return void 0;
1983
+ const credential = parseOAuthCredential(hit.value);
1984
+ return this.expirySkewMs === 0 ? credential : {
1985
+ ...credential,
1986
+ expires: credential.expires - this.expirySkewMs
1987
+ };
1593
1988
  }
1594
- async selectAccount(id) {
1595
- return publicClone(await this.auth.select(id));
1989
+ read(providerId, options) {
1990
+ return this.#enqueue(providerId, () => this.#read(providerId, options), options);
1596
1991
  }
1597
- async removeAccount(id) {
1598
- return publicClone(await this.auth.remove(id));
1992
+ async list(options) {
1993
+ abortIfNeeded(options);
1994
+ return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
1995
+ providerId: PROVIDER$1,
1996
+ type: "oauth"
1997
+ }];
1599
1998
  }
1600
- };
1601
- /** Map the loopback-only DSH Connection channel onto the coordinator. */
1602
- function createCodexRpcHandler(coordinator, options = {}) {
1603
- const openExternal = options.openExternal;
1604
- return async (endpoint, payload, signal) => {
1605
- try {
1606
- signal.throwIfAborted();
1607
- const input = asObject(payload);
1608
- if (endpoint === "status") try {
1609
- return ok(await coordinator.accountStatus({ signal }));
1610
- } catch (error) {
1611
- if (signal.aborted) throw error;
1612
- const [, message] = classifyAccountStatusError(error);
1613
- return accountStatusError(message);
1614
- }
1615
- if (endpoint === "login/start") {
1616
- const started = await coordinator.start({
1617
- method: input.method,
1618
- label: input.label
1999
+ modify(providerId, update, options) {
2000
+ return this.#enqueue(providerId, async () => {
2001
+ if (this.vault !== void 0) {
2002
+ const next = await this.vault.modifyActive(async (current) => {
2003
+ const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
2004
+ ...current,
2005
+ expires: current.expires - this.expirySkewMs
2006
+ };
2007
+ const updated = await update(clone(visible));
2008
+ return updated === void 0 ? void 0 : assertOAuthCredential(updated);
1619
2009
  });
1620
- if (input.openExternal !== true) return ok(started);
1621
- const url = started.authUrl ?? started.deviceCode?.verificationUri;
1622
- if (typeof url !== "string" || openExternal === void 0) return ok({
1623
- ...started,
1624
- externalOpened: false
2010
+ abortIfNeeded(options);
2011
+ return clone(next);
2012
+ }
2013
+ const current = await this.#read(providerId, options);
2014
+ const next = await update(clone(current));
2015
+ abortIfNeeded(options);
2016
+ if (next === void 0) return current;
2017
+ const validated = assertOAuthCredential(next);
2018
+ await this.credentials.set(this.ref, JSON.stringify(validated));
2019
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
2020
+ abortIfNeeded(options);
2021
+ return clone(validated);
2022
+ }, options);
2023
+ }
2024
+ delete(providerId, options) {
2025
+ return this.#enqueue(providerId, async () => {
2026
+ if (this.vault !== void 0) {
2027
+ await this.vault.deleteAll();
2028
+ abortIfNeeded(options);
2029
+ return;
2030
+ }
2031
+ await this.credentials.unset(this.ref);
2032
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
2033
+ abortIfNeeded(options);
2034
+ }, options);
2035
+ }
2036
+ };
2037
+ /** Return only account state that is safe to expose to the browser client. */
2038
+ function createCodexAuthService(models, store, options = {}) {
2039
+ const runLogin = options.runLogin ?? ((run) => run());
2040
+ const accountVault = options.accountVault;
2041
+ const createLoginModels = options.createLoginModels;
2042
+ const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
2043
+ return Object.freeze({
2044
+ async status(options) {
2045
+ const current = await store.read(PROVIDER$1, options);
2046
+ const accounts = await accountVault?.list();
2047
+ if (current === void 0) return {
2048
+ authenticated: false,
2049
+ provider: PROVIDER$1,
2050
+ ...accounts === void 0 ? {} : { accounts }
2051
+ };
2052
+ return {
2053
+ authenticated: true,
2054
+ provider: PROVIDER$1,
2055
+ type: "oauth",
2056
+ expiresAt: current.expires,
2057
+ ...accounts === void 0 ? {} : { accounts }
2058
+ };
2059
+ },
2060
+ login(interaction, input = {}) {
2061
+ if (input.label !== void 0) {
2062
+ if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
2063
+ return runLogin(async () => {
2064
+ const pending = createPendingStore();
2065
+ await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
2066
+ const credential = pending.credential();
2067
+ if (credential === void 0) throw new Error("Codex login did not return credentials");
2068
+ await accountVault.add(input.label, credential);
1625
2069
  });
1626
- try {
1627
- await openExternal(url);
1628
- return ok({
1629
- ...started,
1630
- externalOpened: true
1631
- });
1632
- } catch {
1633
- return ok({
1634
- ...started,
1635
- externalOpened: false
1636
- });
1637
- }
1638
2070
  }
1639
- if (endpoint === "login/status") return ok(coordinator.read(input.id));
1640
- if (endpoint === "login/submit") return ok(await coordinator.submit({
1641
- id: input.id,
1642
- value: input.value
1643
- }));
1644
- if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
1645
- if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
1646
- if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
1647
- if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
1648
- return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
1649
- } catch (error) {
1650
- if (signal.aborted) throw error;
1651
- const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
1652
- return badRequest(message);
2071
+ return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
2072
+ },
2073
+ async select(id) {
2074
+ if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
2075
+ await accountVault.select(id);
2076
+ return this.status();
2077
+ },
2078
+ async remove(id) {
2079
+ if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
2080
+ await accountVault.remove(id);
2081
+ return this.status();
2082
+ },
2083
+ logout(options) {
2084
+ return models.logout(PROVIDER$1, options);
1653
2085
  }
1654
- };
1655
- }
1656
- //#endregion
1657
- //#region src/oauth-network.js
1658
- const execFileAsync = promisify(execFile);
1659
- const CODEX_AUTH_HOST = "auth.openai.com";
1660
- const CODEX_HOSTS = /* @__PURE__ */ new Set([CODEX_AUTH_HOST, "chatgpt.com"]);
1661
- const networkScope = new AsyncLocalStorage();
1662
- let activeScopes = 0;
1663
- let baseFetch;
1664
- let scopedFetch;
1665
- function normalizeProxy(raw) {
1666
- if (typeof raw !== "string" || raw.trim() === "") return void 0;
1667
- const value = raw.trim().includes("://") ? raw.trim() : `http://${raw.trim()}`;
1668
- try {
1669
- const url = new URL(value);
1670
- if (!["http:", "https:"].includes(url.protocol) || url.hostname === "") return void 0;
1671
- return url.toString();
1672
- } catch {
1673
- return;
1674
- }
1675
- }
1676
- function bypassesProxy(hostname, port, rawNoProxy) {
1677
- if (typeof rawNoProxy !== "string" || rawNoProxy.trim() === "") return false;
1678
- return rawNoProxy.split(/[\s,]+/u).some((raw) => {
1679
- const entry = raw.trim().toLowerCase();
1680
- if (entry === "*") return true;
1681
- if (entry === "") return false;
1682
- const match = /^(.*?)(?::(\d+))?$/u.exec(entry);
1683
- const host = match?.[1]?.replace(/^\./u, "");
1684
- const entryPort = match?.[2];
1685
- if (!host || entryPort && entryPort !== port) return false;
1686
- return hostname === host || hostname.endsWith(`.${host}`);
1687
2086
  });
1688
2087
  }
1689
- function proxyFromEnvironment(env = process.env, target = new URL(`https://${CODEX_AUTH_HOST}/`)) {
1690
- if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return void 0;
1691
- return normalizeProxy(env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy);
1692
- }
1693
- function selectWindowsProxy(value) {
1694
- if (typeof value !== "string") return void 0;
1695
- const entries = value.split(";").map((item) => item.trim()).filter(Boolean);
1696
- const https = entries.find((item) => /^https=/iu.test(item));
1697
- const http = entries.find((item) => /^http=/iu.test(item));
1698
- const selected = (https ?? http ?? entries.find((item) => !item.includes("=")))?.replace(/^[^=]+=/u, "");
1699
- return normalizeProxy(selected);
1700
- }
1701
- async function windowsSystemProxy(options = {}) {
1702
- const run = options.execFile ?? execFileAsync;
1703
- const reg = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\reg.exe`;
1704
- const key = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
1705
- try {
1706
- const enabled = await run(reg, [
1707
- "query",
1708
- key,
1709
- "/v",
1710
- "ProxyEnable"
1711
- ], {
1712
- windowsHide: true,
1713
- encoding: "utf8"
1714
- });
1715
- if (!/REG_DWORD\s+0x1\b/iu.test(enabled.stdout)) return void 0;
1716
- const configured = await run(reg, [
1717
- "query",
1718
- key,
1719
- "/v",
1720
- "ProxyServer"
1721
- ], {
1722
- windowsHide: true,
1723
- encoding: "utf8"
1724
- });
1725
- return selectWindowsProxy(/^\s*ProxyServer\s+REG_\w+\s+(.+)$/imu.exec(configured.stdout)?.[1]);
1726
- } catch {
1727
- return;
1728
- }
1729
- }
1730
- async function macSystemProxy(options = {}) {
1731
- const run = options.execFile ?? execFileAsync;
2088
+ //#endregion
2089
+ //#region src/external-url.js
2090
+ const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
2091
+ /** Validate the only external origin this plugin may launch. */
2092
+ function assertCodexAuthUrl(value) {
2093
+ let url;
1732
2094
  try {
1733
- const result = await run("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8" });
1734
- if (!/^\s*HTTPSEnable\s*:\s*1\s*$/imu.test(result.stdout)) return void 0;
1735
- const host = /^\s*HTTPSProxy\s*:\s*(\S+)\s*$/imu.exec(result.stdout)?.[1];
1736
- const port = /^\s*HTTPSPort\s*:\s*(\d+)\s*$/imu.exec(result.stdout)?.[1];
1737
- return normalizeProxy(host && port ? `${host}:${port}` : void 0);
2095
+ url = new URL(value);
1738
2096
  } catch {
1739
- return;
2097
+ throw new Error("Codex auth URL is invalid");
1740
2098
  }
1741
- }
1742
- async function resolveCodexProxy(options = {}) {
1743
- const target = options.target ?? new URL(`https://${CODEX_AUTH_HOST}/`);
1744
- const env = options.env ?? process.env;
1745
- if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return {
1746
- url: void 0,
1747
- source: "bypass"
2099
+ if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
2100
+ if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
2101
+ return url.href;
2102
+ }
2103
+ /** Return a shell-free native opener command for the current desktop. */
2104
+ function commandForCodexAuthUrl(value, platform = process.platform) {
2105
+ const url = assertCodexAuthUrl(value);
2106
+ if (platform === "win32") return {
2107
+ file: "rundll32.exe",
2108
+ args: ["url.dll,FileProtocolHandler", url],
2109
+ shell: false
1748
2110
  };
1749
- const envProxy = proxyFromEnvironment(env, target);
1750
- if (envProxy) return {
1751
- url: envProxy,
1752
- source: "environment"
2111
+ if (platform === "darwin") return {
2112
+ file: "open",
2113
+ args: [url],
2114
+ shell: false
1753
2115
  };
1754
- const platform = options.platform ?? process.platform;
1755
- const system = platform === "win32" ? await windowsSystemProxy(options) : platform === "darwin" ? await macSystemProxy(options) : void 0;
1756
- return system ? {
1757
- url: system,
1758
- source: "system"
1759
- } : {
1760
- url: void 0,
1761
- source: "direct"
2116
+ if (platform === "linux") return {
2117
+ file: "xdg-open",
2118
+ args: [url],
2119
+ shell: false
1762
2120
  };
2121
+ throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
1763
2122
  }
1764
- function bodyBytes(body) {
1765
- if (body === void 0 || body === null) return void 0;
1766
- if (typeof body === "string") return Buffer.from(body);
1767
- if (body instanceof URLSearchParams) return Buffer.from(body.toString());
1768
- if (body instanceof Uint8Array) return Buffer.from(body);
1769
- throw new TypeError("Unsupported Codex OAuth request body");
1770
- }
1771
- function fetchThroughProxy(input, init, proxyUrl) {
1772
- const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
1773
- const body = bodyBytes(init?.body);
1774
- const headers = new Headers(init?.headers);
1775
- if (body && !headers.has("content-length")) headers.set("content-length", String(body.byteLength));
2123
+ function openCodexAuthUrl(value, options = {}) {
2124
+ const command = commandForCodexAuthUrl(value, options.platform);
2125
+ const spawnProcess = options.spawn ?? spawn;
1776
2126
  return new Promise((resolve, reject) => {
1777
- const request$1 = request(target, {
1778
- method: init?.method ?? "GET",
1779
- headers: Object.fromEntries(headers.entries()),
1780
- agent: new HttpsProxyAgent(proxyUrl),
1781
- signal: init?.signal
1782
- }, (response) => {
1783
- const responseHeaders = new Headers();
1784
- for (const [name, value] of Object.entries(response.headers)) if (Array.isArray(value)) value.forEach((item) => responseHeaders.append(name, item));
1785
- else if (value !== void 0) responseHeaders.set(name, value);
1786
- const status = response.statusCode ?? 500;
1787
- const empty = init?.method === "HEAD" || [
1788
- 204,
1789
- 205,
1790
- 304
1791
- ].includes(status);
1792
- resolve(new Response(empty ? null : Readable.toWeb(response), {
1793
- status,
1794
- statusText: response.statusMessage,
1795
- headers: responseHeaders
1796
- }));
2127
+ const child = spawnProcess(command.file, command.args, {
2128
+ detached: true,
2129
+ stdio: "ignore",
2130
+ windowsHide: true,
2131
+ shell: command.shell
2132
+ });
2133
+ child.once("error", reject);
2134
+ child.once("spawn", () => {
2135
+ child.unref();
2136
+ resolve();
1797
2137
  });
1798
- request$1.on("error", reject);
1799
- if (body) request$1.write(body);
1800
- request$1.end();
1801
2138
  });
1802
2139
  }
1803
- async function withCodexNetwork(run, options = {}) {
1804
- if (activeScopes === 0) {
1805
- baseFetch = globalThis.fetch;
1806
- scopedFetch = async (input, init) => {
1807
- const scope = networkScope.getStore();
1808
- if (scope === void 0) return baseFetch(input, init);
1809
- const { options: scopedOptions, allowedHosts, resolved } = scope;
1810
- const proxyFetch = scopedOptions.fetchThroughProxy ?? fetchThroughProxy;
1811
- const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
1812
- if (target.protocol !== "https:" || !allowedHosts.has(target.hostname)) return baseFetch(input, init);
1813
- let proxy = resolved.get(target.hostname);
1814
- if (proxy === void 0) {
1815
- proxy = resolveCodexProxy({
1816
- ...scopedOptions,
1817
- target
1818
- });
1819
- resolved.set(target.hostname, proxy);
2140
+ //#endregion
2141
+ //#region src/login-coordinator.js
2142
+ const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
2143
+ const TERMINAL_PHASES = /* @__PURE__ */ new Set([
2144
+ "authenticated",
2145
+ "failed",
2146
+ "cancelled"
2147
+ ]);
2148
+ const publicClone = (value) => structuredClone(value);
2149
+ const asObject = (value) => value !== null && typeof value === "object" ? value : {};
2150
+ const ok = (value) => ({
2151
+ ok: true,
2152
+ value
2153
+ });
2154
+ const badRequest = (message) => ({
2155
+ ok: false,
2156
+ error: {
2157
+ code: "bad-request",
2158
+ message,
2159
+ details: { issues: [] }
2160
+ }
2161
+ });
2162
+ const accountStatusError = (message) => ({
2163
+ ok: false,
2164
+ error: {
2165
+ code: "internal",
2166
+ message,
2167
+ details: { issues: [] }
2168
+ }
2169
+ });
2170
+ const classifyAccountStatusError = (error) => {
2171
+ const message = error instanceof Error ? error.message : "";
2172
+ if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
2173
+ if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
2174
+ const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
2175
+ if (error?.name === "TimeoutError" || [
2176
+ "TIMEOUT",
2177
+ "ETIMEDOUT",
2178
+ "UND_ERR_CONNECT_TIMEOUT"
2179
+ ].includes(code)) return ["transport", "Codex account status service is unavailable"];
2180
+ if ([
2181
+ "ECONNRESET",
2182
+ "ECONNREFUSED",
2183
+ "ENOTFOUND",
2184
+ "EAI_AGAIN",
2185
+ "NETWORK",
2186
+ "NETWORK_ERROR",
2187
+ "TRANSPORT"
2188
+ ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
2189
+ return ["unknown", "Could not read Codex account status"];
2190
+ };
2191
+ const deferred = () => {
2192
+ let resolve;
2193
+ let reject;
2194
+ return {
2195
+ promise: new Promise((onResolve, onReject) => {
2196
+ resolve = onResolve;
2197
+ reject = onReject;
2198
+ }),
2199
+ resolve,
2200
+ reject
2201
+ };
2202
+ };
2203
+ const publicPrompt = (prompt) => ({
2204
+ type: prompt.type,
2205
+ message: String(prompt.message ?? ""),
2206
+ ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
2207
+ });
2208
+ function classifyLoginFailure(error) {
2209
+ const message = error instanceof Error ? error.message : "";
2210
+ if (/token exchange failed/iu.test(message)) return "token-exchange";
2211
+ if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
2212
+ if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
2213
+ if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
2214
+ if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
2215
+ return "provider";
2216
+ }
2217
+ /** Own one host-side login without exposing tokens to the browser client. */
2218
+ var CodexLoginCoordinator = class {
2219
+ #sessions = /* @__PURE__ */ new Map();
2220
+ #activeId;
2221
+ constructor(auth, options = {}) {
2222
+ this.auth = auth;
2223
+ this.createId = options.createId ?? (() => crypto.randomUUID());
2224
+ }
2225
+ async accountStatus(options) {
2226
+ return publicClone(await this.auth.status(options));
2227
+ }
2228
+ supportState() {
2229
+ const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
2230
+ if (active === void 0) return { phase: "idle" };
2231
+ return {
2232
+ method: active.view.method,
2233
+ phase: active.view.phase,
2234
+ ...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
2235
+ };
2236
+ }
2237
+ async start({ method, label }) {
2238
+ if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
2239
+ if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
2240
+ const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
2241
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
2242
+ active.view = {
2243
+ id: active.view.id,
2244
+ provider: "openai-codex",
2245
+ method: active.view.method,
2246
+ phase: "cancelled",
2247
+ authenticated: false
2248
+ };
2249
+ active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
2250
+ }
2251
+ if (active !== void 0) this.#sessions.delete(active.view.id);
2252
+ const id = this.createId();
2253
+ const ready = deferred();
2254
+ const controller = new AbortController();
2255
+ const session = {
2256
+ controller,
2257
+ prompt: void 0,
2258
+ ready,
2259
+ view: {
2260
+ id,
2261
+ provider: "openai-codex",
2262
+ method,
2263
+ phase: "starting",
2264
+ authenticated: false
2265
+ }
2266
+ };
2267
+ this.#sessions.set(id, session);
2268
+ this.#activeId = id;
2269
+ const publishReady = () => ready.resolve(publicClone(session.view));
2270
+ const interaction = {
2271
+ signal: controller.signal,
2272
+ prompt: async (prompt) => {
2273
+ controller.signal.throwIfAborted();
2274
+ if (prompt.type === "select") return method;
2275
+ if (![
2276
+ "manual_code",
2277
+ "text",
2278
+ "secret"
2279
+ ].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
2280
+ const answer = deferred();
2281
+ session.prompt = answer;
2282
+ session.view = {
2283
+ ...session.view,
2284
+ phase: "waiting_input",
2285
+ prompt: publicPrompt(prompt)
2286
+ };
2287
+ const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
2288
+ controller.signal.addEventListener("abort", abortPrompt, { once: true });
2289
+ prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
2290
+ publishReady();
2291
+ try {
2292
+ return await answer.promise;
2293
+ } finally {
2294
+ controller.signal.removeEventListener("abort", abortPrompt);
2295
+ prompt.signal?.removeEventListener("abort", abortPrompt);
2296
+ if (session.prompt === answer) session.prompt = void 0;
2297
+ }
2298
+ },
2299
+ notify: (event) => {
2300
+ if (controller.signal.aborted) return;
2301
+ if (event.type === "auth_url") session.view = {
2302
+ ...session.view,
2303
+ phase: "waiting_browser",
2304
+ authUrl: assertCodexAuthUrl(event.url),
2305
+ ...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
2306
+ };
2307
+ else if (event.type === "device_code") session.view = {
2308
+ ...session.view,
2309
+ phase: "waiting_device",
2310
+ deviceCode: {
2311
+ userCode: event.userCode,
2312
+ verificationUri: assertCodexAuthUrl(event.verificationUri),
2313
+ ...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
2314
+ ...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
2315
+ }
2316
+ };
2317
+ else session.view = {
2318
+ ...session.view,
2319
+ message: String(event.message ?? "")
2320
+ };
2321
+ publishReady();
1820
2322
  }
1821
- const route = await proxy;
1822
- scopedOptions.onRoute?.(route.source);
1823
- return route.url === void 0 ? baseFetch(input, init) : proxyFetch(input, init, route.url);
1824
2323
  };
1825
- globalThis.fetch = scopedFetch;
2324
+ session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
2325
+ if (controller.signal.aborted) return;
2326
+ const status = await this.auth.status();
2327
+ session.view = {
2328
+ id,
2329
+ provider: "openai-codex",
2330
+ method,
2331
+ phase: "authenticated",
2332
+ authenticated: status.authenticated === true,
2333
+ ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
2334
+ };
2335
+ }).catch(async (error) => {
2336
+ if (controller.signal.aborted) {
2337
+ session.view = {
2338
+ id,
2339
+ provider: "openai-codex",
2340
+ method,
2341
+ phase: "cancelled",
2342
+ authenticated: false
2343
+ };
2344
+ return;
2345
+ }
2346
+ try {
2347
+ if (label !== void 0) throw error;
2348
+ const status = await this.auth.status();
2349
+ if (status.authenticated === true) {
2350
+ session.view = {
2351
+ id,
2352
+ provider: "openai-codex",
2353
+ method,
2354
+ phase: "authenticated",
2355
+ authenticated: true,
2356
+ ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
2357
+ };
2358
+ return;
2359
+ }
2360
+ } catch {}
2361
+ session.view = {
2362
+ id,
2363
+ provider: "openai-codex",
2364
+ method,
2365
+ phase: "failed",
2366
+ authenticated: false,
2367
+ error: "Codex login failed"
2368
+ };
2369
+ session.hostError = error;
2370
+ }).finally(publishReady);
2371
+ return ready.promise;
1826
2372
  }
1827
- activeScopes += 1;
1828
- const scope = {
1829
- options,
1830
- allowedHosts: options.hosts ?? CODEX_HOSTS,
1831
- resolved: /* @__PURE__ */ new Map()
1832
- };
1833
- try {
1834
- return await networkScope.run(scope, run);
1835
- } finally {
1836
- activeScopes -= 1;
1837
- if (activeScopes === 0) {
1838
- if (globalThis.fetch === scopedFetch) globalThis.fetch = baseFetch;
1839
- baseFetch = void 0;
1840
- scopedFetch = void 0;
2373
+ read(id) {
2374
+ const session = this.#sessions.get(id);
2375
+ if (session === void 0) throw new Error("unknown Codex login");
2376
+ return publicClone(session.view);
2377
+ }
2378
+ async submit({ id, value }) {
2379
+ const session = this.#sessions.get(id);
2380
+ if (session === void 0) throw new Error("unknown Codex login");
2381
+ if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
2382
+ if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
2383
+ const answer = session.prompt;
2384
+ session.prompt = void 0;
2385
+ session.view = {
2386
+ ...session.view,
2387
+ phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
2388
+ prompt: void 0
2389
+ };
2390
+ answer.resolve(value);
2391
+ return this.read(id);
2392
+ }
2393
+ async cancel(id) {
2394
+ const session = this.#sessions.get(id);
2395
+ if (session === void 0) throw new Error("unknown Codex login");
2396
+ if (!TERMINAL_PHASES.has(session.view.phase)) {
2397
+ session.view = {
2398
+ id,
2399
+ provider: "openai-codex",
2400
+ method: session.view.method,
2401
+ phase: "cancelled",
2402
+ authenticated: false
2403
+ };
2404
+ session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
1841
2405
  }
2406
+ return this.read(id);
1842
2407
  }
1843
- }
1844
- function classifyTransportError(error) {
1845
- const name = error?.name;
1846
- const code = String(error?.code ?? error?.cause?.code ?? "");
1847
- if (name === "AbortError" || name === "TimeoutError" || /ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT/u.test(code)) return "timeout";
1848
- if (/ENOTFOUND|EAI_AGAIN/u.test(code)) return "dns";
1849
- if (/CERT_|TLS|SSL/u.test(code)) return "tls";
1850
- if (/ECONN|EPIPE|UND_ERR_SOCKET/u.test(code)) return "connection";
1851
- return "network";
1852
- }
1853
- const elapsedBucket = (elapsed) => elapsed < 1e3 ? "under-1s" : elapsed < 5e3 ? "1-5s" : elapsed < 15e3 ? "5-15s" : "over-15s";
1854
- function createCodexNetworkTransport(options = {}) {
1855
- const attempts = /* @__PURE__ */ new Map();
1856
- const now = options.now ?? Date.now;
1857
- const run = async (area, operation) => {
1858
- const startedAt = now();
1859
- let route = attempts.get(area)?.route ?? "direct";
1860
- let routed = false;
2408
+ async logout(options) {
2409
+ if (this.#activeId !== void 0) {
2410
+ const active = this.#sessions.get(this.#activeId);
2411
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
2412
+ }
2413
+ await this.auth.logout(options);
2414
+ return this.accountStatus(options);
2415
+ }
2416
+ async selectAccount(id) {
2417
+ return publicClone(await this.auth.select(id));
2418
+ }
2419
+ async removeAccount(id) {
2420
+ return publicClone(await this.auth.remove(id));
2421
+ }
2422
+ };
2423
+ /** Map the loopback-only DSH Connection channel onto the coordinator. */
2424
+ function createCodexRpcHandler(coordinator, options = {}) {
2425
+ const openExternal = options.openExternal;
2426
+ return async (endpoint, payload, signal) => {
1861
2427
  try {
1862
- const value = await withCodexNetwork(operation, {
1863
- ...options,
1864
- onRoute: (source) => {
1865
- route = source;
1866
- routed = true;
2428
+ signal.throwIfAborted();
2429
+ const input = asObject(payload);
2430
+ if (endpoint === "status") try {
2431
+ return ok(await coordinator.accountStatus({ signal }));
2432
+ } catch (error) {
2433
+ if (signal.aborted) throw error;
2434
+ const [, message] = classifyAccountStatusError(error);
2435
+ return accountStatusError(message);
2436
+ }
2437
+ if (endpoint === "login/start") {
2438
+ const started = await coordinator.start({
2439
+ method: input.method,
2440
+ label: input.label
2441
+ });
2442
+ if (input.openExternal !== true) return ok(started);
2443
+ const url = started.authUrl ?? started.deviceCode?.verificationUri;
2444
+ if (typeof url !== "string" || openExternal === void 0) return ok({
2445
+ ...started,
2446
+ externalOpened: false
2447
+ });
2448
+ try {
2449
+ await openExternal(url);
2450
+ return ok({
2451
+ ...started,
2452
+ externalOpened: true
2453
+ });
2454
+ } catch {
2455
+ return ok({
2456
+ ...started,
2457
+ externalOpened: false
2458
+ });
1867
2459
  }
1868
- });
1869
- if (value instanceof Response && !value.ok) attempts.set(area, {
1870
- status: "failed",
1871
- stage: "http",
1872
- code: "http-error",
1873
- httpStatus: value.status,
1874
- route,
1875
- elapsed: elapsedBucket(now() - startedAt)
1876
- });
1877
- else if (routed || value instanceof Response) attempts.set(area, {
1878
- status: "ok",
1879
- route,
1880
- elapsed: elapsedBucket(now() - startedAt)
1881
- });
1882
- return value;
2460
+ }
2461
+ if (endpoint === "login/status") return ok(coordinator.read(input.id));
2462
+ if (endpoint === "login/submit") return ok(await coordinator.submit({
2463
+ id: input.id,
2464
+ value: input.value
2465
+ }));
2466
+ if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
2467
+ if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
2468
+ if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
2469
+ if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
2470
+ return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
1883
2471
  } catch (error) {
1884
- if (routed) attempts.set(area, {
1885
- status: "failed",
1886
- stage: "transport",
1887
- code: classifyTransportError(error),
1888
- route,
1889
- elapsed: elapsedBucket(now() - startedAt)
1890
- });
1891
- throw error;
2472
+ if (signal.aborted) throw error;
2473
+ const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
2474
+ return badRequest(message);
1892
2475
  }
1893
2476
  };
1894
- return Object.freeze({
1895
- run,
1896
- fetch: (area, input, init) => run(area, () => globalThis.fetch(input, init)),
1897
- snapshot: () => Object.fromEntries([...attempts].map(([area, value]) => [area, { ...value }]))
1898
- });
1899
2477
  }
1900
2478
  //#endregion
1901
2479
  //#region src/pi-ai-runtime.js
@@ -1912,7 +2490,7 @@ const FAST_SERVICE_TIER = "priority";
1912
2490
  * persistence, headers, transport, and model behavior remain owned by the
1913
2491
  * original provider.
1914
2492
  */
1915
- function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, runNetwork = (_area, operation) => operation() } = {}) {
2493
+ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, compaction, runNetwork = (_area, operation) => operation() } = {}) {
1916
2494
  const provider = createOpenAICodexProvider();
1917
2495
  const requestToken = Object.freeze({
1918
2496
  name: "DSH-managed Codex OAuth request token",
@@ -1946,11 +2524,12 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1946
2524
  } },
1947
2525
  ...fast ? { service_tier: FAST_SERVICE_TIER } : {}
1948
2526
  };
1949
- const next = await onPayload?.(preferred, requestModel);
2527
+ const managed = compaction?.preparePayload(preferred, model?.contextWindow) ?? preferred;
2528
+ const next = await onPayload?.(managed, requestModel);
1950
2529
  return {
1951
- ...next ?? preferred,
2530
+ ...next ?? managed,
1952
2531
  ...textVerbosity === void 0 ? {} : { text: {
1953
- ...(next ?? preferred).text ?? {},
2532
+ ...(next ?? managed).text ?? {},
1954
2533
  verbosity: textVerbosity
1955
2534
  } },
1956
2535
  ...fast ? { service_tier: FAST_SERVICE_TIER } : {}
@@ -1975,19 +2554,29 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1975
2554
  contextWindow: requested
1976
2555
  };
1977
2556
  });
1978
- const networkIterable = (factory) => {
2557
+ const networkIterable = (factory, options) => {
1979
2558
  let iterator;
1980
- const getIterator = () => iterator ??= factory()[Symbol.asyncIterator]();
2559
+ let prepared;
2560
+ const step = async (method, value) => {
2561
+ const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
2562
+ return runNetwork("model", () => {
2563
+ iterator ??= factory(compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
2564
+ return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
2565
+ done: true,
2566
+ value
2567
+ }));
2568
+ }, compaction?.networkOptions(request.network) ?? request.network);
2569
+ };
1981
2570
  return {
1982
2571
  [Symbol.asyncIterator]() {
1983
2572
  return this;
1984
2573
  },
1985
- next: (value) => runNetwork("model", () => getIterator().next(value)),
1986
- return: (value) => runNetwork("model", () => getIterator().return?.(value) ?? Promise.resolve({
2574
+ next: (value) => step("next", value),
2575
+ return: (value) => iterator ? step("return", value) : Promise.resolve({
1987
2576
  done: true,
1988
2577
  value
1989
- })),
1990
- throw: (error) => runNetwork("model", () => getIterator().throw?.(error) ?? Promise.reject(error))
2578
+ }),
2579
+ throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
1991
2580
  };
1992
2581
  };
1993
2582
  return Object.freeze({
@@ -1997,14 +2586,14 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1997
2586
  apiKey: requestToken
1998
2587
  }),
1999
2588
  getModels,
2000
- stream: (model, context, options) => networkIterable(() => provider.stream(model, context, withPreferences(model, options))),
2001
- streamSimple: (model, context, options) => networkIterable(() => provider.streamSimple(model, context, withPreferences(model, options)))
2589
+ stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
2590
+ streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
2002
2591
  });
2003
2592
  }
2004
2593
  Object.freeze(["0.82.1", "0.85.1"]);
2005
2594
  //#endregion
2006
2595
  //#region src/version.js
2007
- const PACKAGE_VERSION = "2.1.0";
2596
+ const PACKAGE_VERSION = "2.1.1-beta.2";
2008
2597
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2009
2598
  //#endregion
2010
2599
  //#region src/model-catalog.js
@@ -2029,13 +2618,35 @@ function reasoningMap(levels) {
2029
2618
  for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
2030
2619
  return map;
2031
2620
  }
2621
+ const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
2622
+ function unsupportedCapabilities(value) {
2623
+ const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
2624
+ const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
2625
+ const speeds = capabilityNames([...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []]).filter((tier) => ![
2626
+ "auto",
2627
+ "default",
2628
+ "standard",
2629
+ "fast",
2630
+ "priority"
2631
+ ].includes(tier));
2632
+ return {
2633
+ ...reasoning.length ? { reasoning } : {},
2634
+ ...inputs.length ? { inputs } : {},
2635
+ ...speeds.length ? { speeds } : {}
2636
+ };
2637
+ }
2032
2638
  function visibleModel(value) {
2033
2639
  if (!record$4(value)) return void 0;
2034
2640
  const id = nonEmpty$2(value.slug);
2035
2641
  if (id === void 0 || value.visibility !== "list") return void 0;
2036
2642
  const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
2037
2643
  const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
2644
+ const unsupported = unsupportedCapabilities({
2645
+ ...value,
2646
+ supported_reasoning_levels: supported
2647
+ });
2038
2648
  return {
2649
+ ...Object.keys(unsupported).length ? { unsupported } : {},
2039
2650
  id,
2040
2651
  name: nonEmpty$2(value.display_name) ?? id,
2041
2652
  description: nonEmpty$2(value.description),
@@ -2179,6 +2790,10 @@ function createOfficialModelCatalog(options = {}) {
2179
2790
  getModels: (fallback) => models ?? fallback,
2180
2791
  metadata: (modelId) => metadata.get(modelId),
2181
2792
  revision: () => revision,
2793
+ capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
2794
+ model: model.id,
2795
+ ...structuredClone(model.unsupported)
2796
+ })),
2182
2797
  status: () => ({
2183
2798
  source: models === void 0 ? "fallback" : "online",
2184
2799
  refresh: refreshStatus
@@ -3081,6 +3696,22 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
3081
3696
  }
3082
3697
  const preference = preferences.status();
3083
3698
  const catalog = modelCatalog?.status?.();
3699
+ const gaps = (modelCatalog?.capabilityGaps?.() ?? []).slice(0, 20).flatMap((value) => {
3700
+ if (!value || typeof value.model !== "string" || !/^[a-z][a-z0-9._-]{0,79}$/u.test(value.model)) return [];
3701
+ const fields = Object.fromEntries([
3702
+ "reasoning",
3703
+ "inputs",
3704
+ "speeds"
3705
+ ].flatMap((key) => {
3706
+ const names = [...new Set((Array.isArray(value[key]) ? value[key] : []).filter((name) => typeof name === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(name)))].slice(0, 16);
3707
+ return names.length ? [[key, names]] : [];
3708
+ }));
3709
+ return Object.keys(fields).length ? [{
3710
+ model: value.model,
3711
+ ...fields
3712
+ }] : [];
3713
+ });
3714
+ if (gaps.length) issues.push({ code: "catalog-capabilities-not-adapted" });
3084
3715
  return {
3085
3716
  schemaVersion: 3,
3086
3717
  package: "dsh-codex-subscription",
@@ -3100,7 +3731,8 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
3100
3731
  "failed"
3101
3732
  ].includes(catalog.refresh) ? { catalog: {
3102
3733
  source: catalog.source,
3103
- refresh: catalog.refresh
3734
+ refresh: catalog.refresh,
3735
+ ...gaps.length ? { unsupported: gaps } : {}
3104
3736
  } } : {},
3105
3737
  configuration: {
3106
3738
  contextMode: preference.contextMode,
@@ -4205,7 +4837,7 @@ function apply(ctx) {
4205
4837
  ...Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, value]) => [key, z.boolean().default(value)])),
4206
4838
  [CUSTOM_CONTEXT_OVERRIDES_FIELD]: z.dict(z.number().step(1).min(1).max(MAX_CONTEXT_BUDGET)).default({}),
4207
4839
  [SEARCH_MODE_FIELD]: z.union(SEARCH_MODES).default("live"),
4208
- [SEARCH_DOMAINS_FIELD]: z.transform(z.array(z.string()).max(20), (value) => readCapabilitySettings({ searchDomains: value }).searchDomains).default([]),
4840
+ [SEARCH_DOMAINS_FIELD]: z.transform(z.array(z.string()).max(20), normalizeSearchDomains).default([]),
4209
4841
  ...Object.fromEntries(QUOTA_THRESHOLD_FIELDS.map((key) => [key, z.number().step(1).min(1).max(100).default(20)])),
4210
4842
  [QUOTA_ALERTS_FIELD]: z.union(QUOTA_ALERT_MODES).default("important"),
4211
4843
  [LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
@@ -4226,13 +4858,26 @@ function apply(ctx) {
4226
4858
  });
4227
4859
  const baseProvider = createOpenAICodexProvider();
4228
4860
  let resolveAuth = async () => void 0;
4861
+ let subagentBackend;
4229
4862
  const modelCatalog = createOfficialModelCatalog({
4230
4863
  getAuth: (options) => resolveAuth(options),
4231
4864
  readCredential: (options) => store.read(PROVIDER, options),
4232
4865
  baseModels: () => baseProvider.getModels(),
4233
4866
  fetch: (input, init) => network.fetch("catalog", input, init)
4234
4867
  });
4868
+ const connection = createSubscriptionConnection({ resolveMode: () => settings.get().connectionMode });
4869
+ const compaction = createCompactionBridge({
4870
+ enabled: () => settings.get().compactionMode === "cloud",
4871
+ accountScope: async () => {
4872
+ await resolveAuth();
4873
+ const credential = await store.read(PROVIDER);
4874
+ return credential?.type === "oauth" ? credential.accountId : void 0;
4875
+ }
4876
+ });
4877
+ ctx.effect(() => () => connection.dispose());
4235
4878
  const provider = openaiCodexSubscriptionProvider({
4879
+ connection,
4880
+ compaction,
4236
4881
  resolveSpeedMode: () => settings.get()[SPEED_MODE_FIELD],
4237
4882
  resolveOutputVerbosity: () => normalizeOutputVerbosity(settings.get()[OUTPUT_VERBOSITY_FIELD]),
4238
4883
  resolveContextMode: () => normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]),
@@ -4248,6 +4893,10 @@ function apply(ctx) {
4248
4893
  });
4249
4894
  const preferences = {
4250
4895
  status: () => ({
4896
+ compactionMode: settings.get().compactionMode ?? "dsh",
4897
+ connectionMode: settings.get().connectionMode ?? "sse",
4898
+ subagentBackend: settings.get().subagentBackend ?? "dsh",
4899
+ subagentBackendAvailable: subagentBackend !== void 0,
4251
4900
  ...readCapabilitySettings(settings.get()),
4252
4901
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]),
4253
4902
  [SEARCH_PROVIDER_FIELD]: settings.get()[SEARCH_PROVIDER_FIELD],
@@ -4262,7 +4911,15 @@ function apply(ctx) {
4262
4911
  fastModels: provider.getModels().filter((model) => modelCatalog.metadata(model.id)?.supportsFast ?? supportsCodexFastMode(model.id)).map((model) => model.id),
4263
4912
  writable: ctx.settings.writable
4264
4913
  }),
4265
- update: (patch) => settings.update(patch)
4914
+ update: async (patch) => {
4915
+ if (Object.hasOwn(patch, "subagentBackend")) {
4916
+ if (!subagentBackend) throw new Error("DSH subagent services are unavailable");
4917
+ await subagentBackend.select(patch.subagentBackend);
4918
+ }
4919
+ const rest = { ...patch };
4920
+ delete rest.subagentBackend;
4921
+ if (Object.keys(rest).length) await settings.update(rest);
4922
+ }
4266
4923
  };
4267
4924
  const authModels = createModels({ credentials: store });
4268
4925
  authModels.setProvider(provider);
@@ -4295,6 +4952,53 @@ function apply(ctx) {
4295
4952
  return profileSnapshot;
4296
4953
  };
4297
4954
  resolveAuth = () => authModels.getAuth(PROVIDER);
4955
+ ctx.inject([
4956
+ "subagents",
4957
+ "subprocess",
4958
+ "sandboxPolicy"
4959
+ ], (scoped) => {
4960
+ const instance = createSubscriptionSubagent({
4961
+ ctx: scoped,
4962
+ nativeHome: dshHomePath("state", "codex-subscription", "native-subagent"),
4963
+ resolveAuth,
4964
+ store,
4965
+ refresh: (credential) => network.run("oauth", () => baseProvider.auth.oauth.refresh(credential)),
4966
+ loadRuntime: loadSubagentRuntime
4967
+ });
4968
+ scoped.subagents.registerProvider(instance.provider);
4969
+ const switcher = createSubagentBackendSwitcher({
4970
+ entries: () => scoped.loader.entries(),
4971
+ prepare: instance.prepare,
4972
+ persist: (mode) => settings.update({ subagentBackend: mode })
4973
+ });
4974
+ const unconfigure = scoped.on("internal/config", function(_config, next) {
4975
+ return switcher.configure(this, next());
4976
+ }, { global: true });
4977
+ let requested = "dsh";
4978
+ const select = async (mode) => {
4979
+ requested = mode;
4980
+ try {
4981
+ await switcher.select(mode);
4982
+ } catch (error) {
4983
+ requested = settings.get().subagentBackend ?? "dsh";
4984
+ throw error;
4985
+ }
4986
+ };
4987
+ subagentBackend = { select };
4988
+ const sync = (value) => {
4989
+ const mode = value.subagentBackend ?? "dsh";
4990
+ if (mode !== requested) select(mode).catch(() => scoped.logger.warn("Could not switch the subscription subagent backend"));
4991
+ };
4992
+ scoped.loader.await().then(() => sync(settings.get())).catch(() => scoped.logger.warn("Could not initialize the subscription subagent backend"));
4993
+ const unwatch = settings.watch(sync);
4994
+ scoped.effect(() => async () => {
4995
+ unwatch();
4996
+ unconfigure();
4997
+ subagentBackend = void 0;
4998
+ instance.dispose();
4999
+ await switcher.dispose();
5000
+ }, "codex-subscription: subagent backend");
5001
+ });
4298
5002
  const adapterAuth = Object.freeze({
4299
5003
  credentials: store,
4300
5004
  authContext: Object.freeze({
@@ -4326,7 +5030,7 @@ function apply(ctx) {
4326
5030
  auth: adapterAuth,
4327
5031
  resolveAttachments: () => ctx.get?.("attachments")
4328
5032
  });
4329
- ctx.llm.registerAdapter([PROVIDER], adapter);
5033
+ ctx.llm.registerAdapter([PROVIDER], compaction.wrapAdapter(adapter));
4330
5034
  const currentAgent = () => ctx.get?.("agents")?.currentInitiator?.();
4331
5035
  const codexSearch = createCodexSearchProvider({
4332
5036
  resolvePreferences: () => readCapabilitySettings(settings.get()),