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

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