super-dsh 0.1.10-e → 0.1.11-a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +38 -2
  2. package/agent-agy/dist/agent.js +19 -1
  3. package/agent-claude/dist/agent.js +23 -5
  4. package/agent-codex/dist/adapter.js +3 -2
  5. package/agent-codex/dist/agent.js +41 -7
  6. package/agent-codex/dist/app-server-client.js +9 -2
  7. package/agent-codex/dist/app-server-protocol.js +6 -2
  8. package/agent-codex/dist/app-server-wire.js +29 -24
  9. package/agent-codex/dist/codex-launch.js +10 -32
  10. package/agent-codex/dist/codex-native-config.js +9 -3
  11. package/agent-codex/dist/codex-oauth.js +153 -0
  12. package/agent-codex/dist/codex-provider-gateway.js +36 -38
  13. package/agent-codex/dist/codex-providers.js +20 -7
  14. package/agent-codex/dist/codex-runtime-fetch.js +293 -0
  15. package/agent-codex/dist/codex-runtime.js +120 -0
  16. package/agent-codex/dist/index.js +20 -12
  17. package/agent-codex/package.json +8 -3
  18. package/agent-codex-sdk/dist/adapter.js +3 -2
  19. package/agent-codex-sdk/dist/agent.js +35 -1
  20. package/agent-codex-sdk/dist/codex-native-config.js +9 -3
  21. package/agent-codex-sdk/dist/codex-oauth.js +153 -0
  22. package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
  23. package/agent-codex-sdk/dist/codex-providers.js +20 -7
  24. package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
  25. package/agent-codex-sdk/dist/index.js +18 -11
  26. package/agent-codex-sdk/dist/sdk-runtime.js +3 -3
  27. package/agent-hermes/dist/agent.js +46 -2
  28. package/agent-hermes/dist/hermes-client.js +49 -1
  29. package/agent-omp/dist/agent.js +17 -4
  30. package/agent-omp-sdk/dist/agent.d.ts +11 -0
  31. package/agent-omp-sdk/dist/agent.js +17 -4
  32. package/agent-pi/dist/agent.js +33 -1
  33. package/client/index.js +10 -7
  34. package/package.json +7 -5
  35. package/superd-hub/dist/client/agent-settings.d.ts +1 -1
  36. package/superd-hub/dist/client/agent-settings.js +2 -2
  37. package/superd-hub/dist/connection-trust.d.ts +8 -0
  38. package/superd-hub/dist/connection-trust.js +16 -0
  39. package/superd-hub/dist/web-trust.d.ts +14 -42
  40. package/superd-hub/dist/web-trust.js +18 -43
  41. package/superd-hub/dist/world-mount.d.ts +6 -5
  42. package/superd-hub/dist/world-mount.js +6 -6
  43. package/superd-hub/lib/client/index.js +10 -7
  44. package/superd-hub/package.json +5 -1
  45. package/agent-codex/README.md +0 -179
  46. package/agent-codex-sdk/README.md +0 -79
  47. package/agent-hermes/README.md +0 -145
  48. package/agent-omp/README.md +0 -94
  49. package/agent-omp-sdk/README.md +0 -103
  50. package/agent-pi/README.md +0 -67
package/README.md CHANGED
@@ -14,7 +14,7 @@ the foreign store.
14
14
  ## Install
15
15
 
16
16
  ```bash
17
- dsh plugin --profile <name> add super-dsh@0.1.0
17
+ dsh plugin --profile <name> add super-dsh@0.1.11
18
18
  ```
19
19
 
20
20
  That single package composes the whole line (`@pgmi-builds/superd-hub` plus the
@@ -22,17 +22,53 @@ six primary `@pgmi-builds/agent-*` packages and three OPTIONAL SDK halves).
22
22
  Restart the profile and the
23
23
  home under `<DSH_HOME>/agents/<label>/` on first boot.
24
24
 
25
+ ## Remote web access
26
+
27
+ To use the same authenticated UI through a reverse proxy or LAN relay, enable
28
+ this profile patch:
29
+
30
+ ```yaml
31
+ - id: superd-web-trust
32
+ config:
33
+ trustAllHosts: true
34
+ ```
35
+
36
+ This skips Host/Origin restrictions and enables browser ownership for every
37
+ hostname. Native token-to-cookie login, cookie expiry, and authority binding
38
+ remain in effect. The default is `false`, preserving declared-authority mode.
39
+ It does not change the server's listening address: keep loopback behind Caddy
40
+ and a LAN relay. The test launcher enables this mode without `--host` or
41
+ `--trusted-host` and starts the relay on every test port, including 4999.
42
+
25
43
  ## Runtimes
26
44
 
27
45
  | Label | Runtime | Integration |
28
46
  |---|---|---|
29
47
  | OMP | OMP (`~/.omp`) | host `omp` binary over RPC (`omp --mode rpc`, NDJSON v1), lazy per session |
30
- | Codex | Codex CLI (`~/.codex`) | adapter world plugin |
48
+ | Codex | Codex app-server (`~/.codex`) | desktop core first, CLI and bundled core fallback |
31
49
  | Claude | Claude Code (`~/.claude`) | standalone adapter, joined by the hub |
32
50
  | Pi | pi coding agent (`~/.pi`) | in-process SDK |
33
51
  | Hermes | Hermes (`~/.hermes`) | gateway child |
34
52
  | Antigravity | agy SDK | Python bridge (`bridge/agy_bridge.py`) |
35
53
 
54
+ ### Codex in 0.1.11
55
+
56
+ Codex discovers the local OpenAI OAuth login as a read-only provider in the
57
+ model selector. Selecting it explicitly overrides a third-party provider in the
58
+ native config, while credentials and native configuration stay in the native
59
+ home. Third-party providers remain selectable per session.
60
+
61
+ The adapter prefers the installed desktop core, then the CLI, then the bundled
62
+ 0.160.0 core (`CODEX_BIN` overrides discovery). Desktop host browser and computer
63
+ use services are separate and are not attached by this adapter.
64
+
65
+ **Provider compatibility:** each provider defines its own message, reasoning,
66
+ and encrypted-state formats. Selecting another provider does not guarantee
67
+ that it can consume the existing session history. Successful continuation
68
+ across endpoints is an observed convenience, not an adapter guarantee.
69
+ The adapter surfaces provider errors; if the selected provider rejects the
70
+ history, start a new session with that provider.
71
+
36
72
  ### Opt-in SDK halves
37
73
 
38
74
  Three runtimes also ship a preserved in-process SDK transport beside the primary
@@ -129,9 +129,27 @@ export class AgyAgent {
129
129
  this.closeStep();
130
130
  this.commitStepStart();
131
131
  const callId = ToolCallId(ev.id || `agy-${this.dashTurn}-${this.step}`);
132
+ const argumentsJson = ev.args || "{}";
133
+ // Advertisement-before-execution (validator invariant, 2026-10-04 dev3
134
+ // omp/hermes repair family): agy assistant frames carry only
135
+ // reasoning/text — never tool-call blocks — so every tool/call needs
136
+ // a minimal advertisement assistant frame first; arguments are copied
137
+ // verbatim into both frames (validator: lifecycle arguments ≡
138
+ // advertised, strict string equality).
139
+ const advertisement = createAssistantMessage({
140
+ content: [{ type: "tool-call", id: callId, name: ev.name, arguments: argumentsJson }],
141
+ source: {
142
+ provider: this.options.provider ?? "agy",
143
+ model: this.options.model ?? "",
144
+ },
145
+ });
146
+ this.session.append("assistant/message", {
147
+ turn: this.dashTurn, step: this.step,
148
+ message: advertisement, stream: [],
149
+ }, { surfaceOp: "append" });
132
150
  this.session.append("tool/call", {
133
151
  turn: this.dashTurn, step: this.step,
134
- callId, name: ev.name, arguments: ev.args || "{}",
152
+ callId, name: ev.name, arguments: argumentsJson,
135
153
  });
136
154
  this.unpairedCalls.push({ callId, name: ev.name });
137
155
  }
@@ -969,8 +969,12 @@ export class ClaudeAgent {
969
969
  * projection carried — so the `tool_start` flush lands a same-attempt
970
970
  * sample and nothing ever stashes across turns.
971
971
  */
972
- flushAssistant(usage) {
973
- if (this.reasoningBuffer === "" && this.textBuffer === "") {
972
+ flushAssistant(usage, toolCall) {
973
+ // A toolCall rides the SAME frame (advertise+usage on one attempt): the
974
+ // token-meter fold requires every step's assistant frame to carry a usage
975
+ // sample, so a separate no-usage advertisement frame would turn the
976
+ // turn's whole usage disclosure undefined.
977
+ if (toolCall === undefined && this.reasoningBuffer === "" && this.textBuffer === "") {
974
978
  if (usage !== undefined)
975
979
  trace("flush had usage but no buffered content — sample dropped (no message carries it)");
976
980
  return;
@@ -980,6 +984,9 @@ export class ClaudeAgent {
980
984
  blocks.push({ type: "reasoning", text: this.reasoningBuffer });
981
985
  if (this.textBuffer !== "")
982
986
  blocks.push({ type: "text", text: this.textBuffer });
987
+ if (toolCall !== undefined) {
988
+ blocks.push({ type: "tool-call", id: ToolCallId(toolCall.callId), name: toolCall.name, arguments: toolCall.arguments });
989
+ }
983
990
  this.reasoningBuffer = "";
984
991
  this.textBuffer = "";
985
992
  const effectiveUsage = usage ?? this.capturedUsage;
@@ -1006,13 +1013,23 @@ export class ClaudeAgent {
1006
1013
  this.streamBridge = undefined;
1007
1014
  }
1008
1015
  appendToolCall(callId, name, args) {
1009
- this.commitStepStart();
1016
+ // The tool-call block rides the SAME assistant/message as any pending
1017
+ // text (native advertise+usage shape): one attempt per step with the
1018
+ // usage sample AND the advertisement on one frame. A separate no-usage
1019
+ // advertisement frame would satisfy the format validator but break the
1020
+ // token-meter fold (every step's assistant frame needs a usage sample).
1021
+ const argumentsJson = JSON.stringify(args ?? {});
1022
+ this.flushAssistant(undefined, {
1023
+ callId,
1024
+ name,
1025
+ arguments: argumentsJson,
1026
+ });
1010
1027
  this.session.append("tool/call", {
1011
1028
  turn: this.dashTurn,
1012
1029
  step: this.step,
1013
1030
  callId: ToolCallId(callId),
1014
1031
  name,
1015
- arguments: JSON.stringify(args ?? {}),
1032
+ arguments: argumentsJson,
1016
1033
  });
1017
1034
  }
1018
1035
  appendToolResult(callId, content, isError) {
@@ -1193,7 +1210,8 @@ export class ClaudeAgent {
1193
1210
  // RC-6: the mid-turn flush closes the attempt WITH the sample the
1194
1211
  // message itself reported (captured above / on the assistant events).
1195
1212
  this.captureProvenance(event);
1196
- this.flushAssistant();
1213
+ // flushes pending text WITH the tool-call block as one assistant
1214
+ // message (advertise+usage on one attempt — see appendToolCall).
1197
1215
  this.appendToolCall(callId, name, event.arguments);
1198
1216
  break;
1199
1217
  }
@@ -1,3 +1,5 @@
1
+ import { readEffectiveProviderStore } from "./codex-oauth.js";
2
+ import { resolveCodexHome } from "./codex-store.js";
1
3
  /**
2
4
  * Codex-backed `LlmAdapter` registered on `ctx.llm` by the Codex provider.
3
5
  *
@@ -23,7 +25,6 @@
23
25
  * @module agent-codex/adapter
24
26
  */
25
27
  import { LlmAdapter, LlmError, } from "@deepseek-ai/dsh-llm";
26
- import { readProviderStore } from "./codex-providers.js";
27
28
  /**
28
29
  * The single route every Codex model was served under before the multi-provider
29
30
  * rework. No longer registered (a session pinned to it falls back to whatever
@@ -41,7 +42,7 @@ export function routeIds(store) {
41
42
  }
42
43
  /** The provider entry a route id names, or `undefined`. */
43
44
  export function providerFor(worldHome, providerId) {
44
- return readProviderStore(worldHome).providers.find((entry) => entry.id === providerId);
45
+ return readEffectiveProviderStore(worldHome, resolveCodexHome()).providers.find((entry) => entry.id === providerId);
45
46
  }
46
47
  export class CodexLlmAdapter extends LlmAdapter {
47
48
  worldHome;
@@ -289,6 +289,11 @@ export class CodexAgent {
289
289
  */
290
290
  #remoteQueue = [];
291
291
  #bridgedToolResults = new Set();
292
+ /** callIds already advertised by an appended assistant/message frame — the
293
+ * strict validator refuses any `tool/call` whose callId no assistant frame
294
+ * advertised, and requires the execution arguments to equal the advertised
295
+ * string (2026-10-04 dev3 omp/hermes repair: same write-side family). */
296
+ #advertisedToolCalls = new Set();
292
297
  /**
293
298
  * The open turn's FINAL assistant message, buffered from `message_end`
294
299
  * until `turn_end` so the SAME turn's usage can land on it (the DSH
@@ -711,9 +716,14 @@ export class CodexAgent {
711
716
  // An unknown provider id (a session pinned before the multi-provider rework)
712
717
  // maps to "no injection": the user's own config.toml is the honest answer.
713
718
  const launchSpec = launch ?? NATIVE_LAUNCH;
714
- const providerId = launch === undefined ? null : route?.provider ?? null;
719
+ const providerId = launch === undefined ? null : launch.runtimeProvider ?? route?.provider ?? null;
715
720
  const client = this.#client;
716
721
  const key = `${providerId ?? "native"}/${route?.model ?? ""}`;
722
+ // Pin before ensureStarted: the first thread and turn otherwise inherit
723
+ // the native model captured at construction, even with a new provider.
724
+ if (route !== undefined) {
725
+ await client.setModel(route.provider, route.model);
726
+ }
717
727
  if (!client.spawned && client.threadId === null) {
718
728
  client.applyLaunch(launchSpec, providerId);
719
729
  await client.ensureStarted();
@@ -726,11 +736,6 @@ export class CodexAgent {
726
736
  else if (client.appliedProvider !== providerId) {
727
737
  await client.reattach(launchSpec, providerId, route?.model);
728
738
  }
729
- else if (route !== undefined) {
730
- await client.setModel(route.provider, route.model).catch((error) => {
731
- trace(`set_model ${key} failed: ${String(error)}`);
732
- });
733
- }
734
739
  this.#routeKey = key;
735
740
  }
736
741
  /**
@@ -1043,7 +1048,32 @@ export class CodexAgent {
1043
1048
  case "tool_execution_start": {
1044
1049
  const callId = String(event.toolCallId ?? "");
1045
1050
  const name = String(event.toolName ?? "");
1051
+ const argumentsJson = String(event.argumentsJson ?? JSON.stringify(event.args ?? {}));
1046
1052
  this.#commitStepStart();
1053
+ // Advertisement-before-execution (validator invariant, 2026-10-04 dev3
1054
+ // omp/hermes repair family): unless a real assistant frame advertised
1055
+ // this callId, bridge a minimal assistant/message carrying the
1056
+ // tool-call block — arguments copied verbatim into both frames. The
1057
+ // synthesized source mirrors #appendAssistantMessage's open-turn
1058
+ // fallback chain.
1059
+ if (!this.#advertisedToolCalls.has(callId)) {
1060
+ const advertisement = createAssistantMessage({
1061
+ content: [{ type: "tool-call", id: ToolCallId(callId), name, arguments: argumentsJson }],
1062
+ source: {
1063
+ provider: String(this.#turnRoute?.provider ?? this.options.provider ?? ""),
1064
+ model: String(this.#turnRoute?.model ?? this.options.model ?? ""),
1065
+ },
1066
+ });
1067
+ this.session.append("assistant/message", {
1068
+ turn: this.#dashTurn,
1069
+ step: this.#step,
1070
+ message: advertisement,
1071
+ stream: [],
1072
+ }, {
1073
+ surfaceOp: "append",
1074
+ });
1075
+ this.#advertisedToolCalls.add(callId);
1076
+ }
1047
1077
  this.session.append("tool/call", {
1048
1078
  turn: this.#dashTurn,
1049
1079
  step: this.#step,
@@ -1052,7 +1082,7 @@ export class CodexAgent {
1052
1082
  // The DSH payload requires the RAW JSON string the model produced;
1053
1083
  // the wire carries it verbatim for MCP calls and renders it from the
1054
1084
  // discrete fields for the other item shapes.
1055
- arguments: String(event.argumentsJson ?? JSON.stringify(event.args ?? {})),
1085
+ arguments: argumentsJson,
1056
1086
  });
1057
1087
  break;
1058
1088
  }
@@ -1164,6 +1194,10 @@ export class CodexAgent {
1164
1194
  }
1165
1195
  #appendAssistantMessage(message, usage) {
1166
1196
  const content = convertContent(message.content);
1197
+ for (const block of content) {
1198
+ if (block.type === "tool-call")
1199
+ this.#advertisedToolCalls.add(block.id);
1200
+ }
1167
1201
  const assistant = createAssistantMessage({
1168
1202
  content,
1169
1203
  source: {
@@ -440,11 +440,18 @@ export class CodexAppServerClient {
440
440
  // crash still fails the client.
441
441
  const generation = ++this.#transportGeneration;
442
442
  const launch = this.#launch;
443
+ // This is an independent Super D consumer, including when its parent
444
+ // happens to be a desktop task. The desktop host's attribution and
445
+ // tools pipe belong to that host, not to this new app-server.
446
+ const childEnv = { ...process.env, ...launch.env };
447
+ delete childEnv.CODEX_INTERNAL_ORIGINATOR_OVERRIDE;
448
+ delete childEnv.CODEX_APP_TOOLS_PIPE_PATH;
443
449
  transport = transportFactory({
444
450
  ...(this.#options.cwd === undefined ? {} : { cwd: this.#options.cwd }),
445
451
  ...(this.#options.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: this.#options.requestTimeoutMs }),
452
+ ...(this.#options.worldHome === undefined ? {} : { worldHome: this.#options.worldHome }),
446
453
  args: ["app-server", ...launch.argv],
447
- env: { ...process.env, ...launch.env },
454
+ env: childEnv,
448
455
  onNotification: (notification) => this.#onNotification(notification),
449
456
  onServerRequest: (request) => this.#onServerRequest(request),
450
457
  onExit: (info) => {
@@ -456,7 +463,7 @@ export class CodexAppServerClient {
456
463
  }
457
464
  this.#transport = transport;
458
465
  if (first) {
459
- transport.start();
466
+ await transport.start();
460
467
  const info = asRecord(await transport.request("initialize", {
461
468
  clientInfo: { name: "superd-agent-codex", title: "Super D agent-codex", version: "0.1.0" },
462
469
  capabilities: { experimentalApi: true, requestAttestation: false },
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Narrow hand-written types for the codex `app-server` JSONL JSON-RPC wire.
3
3
  *
4
- * DERIVED FROM: the `codex app-server generate-ts --out <dir>` output for the
5
- * `@openai/codex` version this package pins (0.154.0). That tree is ~617 files;
4
+ * DERIVED FROM: the `codex app-server generate-ts --out <dir>` output for
5
+ * `@openai/codex@0.154.0` (the SDK line's tested pair). The app-server line's
6
+ * runtime target is the exact-pin `@openai/codex@0.160.0`, lazily fetched into
7
+ * the world home (codex-runtime-fetch.ts) — never an install-time dependency.
8
+ * That tree is ~617 files;
9
+
6
10
  * this module is a hand-written, deliberately narrow subset covering exactly
7
11
  * the envelopes, requests, notifications and item shapes
8
12
  * `src/app-server-client.ts` and `src/app-server-events.ts` consume. Each
@@ -7,7 +7,10 @@
7
7
  * `{"id":…,"result":…}` / `{"id":…,"error":{…}}`).
8
8
  *
9
9
  * This module owns exactly four things and nothing else:
10
- * 1. executable resolution (pin-faithful: the package's own `@openai/codex`),
10
+ * 1. runtime selection (override → PATH CLI → desktop core → fetched world-home
11
+ * runtime — codex-runtime-fetch.ts; the pinned package is NEVER an
12
+ * install-time dependency, 2026-10-06 ruling),
13
+
11
14
  * 2. process spawn + line framing + per-id request/response correlation,
12
15
  * 3. dispatch of server-initiated notifications and server-requests,
13
16
  * 4. teardown (process GROUP kill — the app-server may spawn stdio MCP
@@ -22,7 +25,9 @@
22
25
  * process, and the client calls it on first real work.
23
26
  */
24
27
  import { spawn } from "node:child_process";
25
- import { createRequire } from "node:module";
28
+ import { createCodexRuntimeDeps, ensureCodexRuntime } from "./codex-runtime-fetch.js";
29
+ import { resolveCodexRuntime } from "./codex-runtime.js";
30
+ export { resolveCodexCommand, resolveCodexRuntime } from "./codex-runtime.js";
26
31
  /** Per-request protocol error surfaced by the server (JSON-RPC `error` object). */
27
32
  export class CodexWireError extends Error {
28
33
  code;
@@ -37,23 +42,6 @@ export class CodexWireError extends Error {
37
42
  /** Server overload: the documented retryable backpressure code. */
38
43
  export const SERVER_OVERLOADED_CODE = -32001;
39
44
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
40
- /** The codex entry script this package pins (`@openai/codex/bin/codex.js`). */
41
- export function resolveCodexCommand() {
42
- const override = process.env.CODEX_BIN;
43
- if (override !== undefined && override.trim() !== "")
44
- return override;
45
- const require = createRequire(import.meta.url);
46
- try {
47
- // Pin-faithful: the SAME `@openai/codex` this package depends on, so the
48
- // wire protocol and the binary version can never drift apart. Set
49
- // CODEX_BIN to override (tests).
50
- return require.resolve("@openai/codex/bin/codex.js");
51
- }
52
- catch (error) {
53
- throw new Error("cannot locate the pinned codex CLI (@openai/codex/bin/codex.js): "
54
- + `${error instanceof Error ? error.message : String(error)}. Install the package or set CODEX_BIN.`);
55
- }
56
- }
57
45
  /** True for a plain JSON object (never arrays/null). */
58
46
  function asRecord(value) {
59
47
  if (value === null || typeof value !== "object" || Array.isArray(value))
@@ -85,22 +73,25 @@ export class CodexAppServerTransport {
85
73
  }
86
74
  /**
87
75
  * Spawn the child exactly once. Idempotent.
88
- * @throws when the child cannot be spawned at all (bad executable).
76
+ * @throws when no runtime can be discovered or fetched, or the child cannot
77
+ * be spawned at all (bad executable).
89
78
  */
90
- start() {
79
+ async start() {
91
80
  if (this.#child !== undefined)
92
81
  return;
93
- const command = this.#options.command ?? resolveCodexCommand();
82
+ const env = this.#options.env ?? { ...process.env };
83
+ const runtime = await this.#resolveRuntime(env);
94
84
  const args = [...(this.#options.args ?? ["app-server"])];
95
85
  // `detached` gives the child its own process group so teardown can kill
96
86
  // the whole tree (stdio MCP descendants included), not just the root.
97
- const child = spawn(process.execPath, [command, ...args], {
87
+ const child = spawn(runtime.nodeScript ? process.execPath : runtime.command, runtime.nodeScript ? [runtime.command, ...args] : args, {
98
88
  cwd: this.#options.cwd,
99
- env: this.#options.env ?? { ...process.env },
89
+ env,
100
90
  stdio: ["pipe", "pipe", "pipe"],
101
91
  detached: true,
102
92
  });
103
93
  this.#child = child;
94
+ this.#options.onStderr?.(`codex runtime source=${runtime.source} command=${runtime.command} mode=${runtime.nodeScript ? "node" : "native"}\n`);
104
95
  child.stdout?.setEncoding("utf8");
105
96
  child.stderr?.setEncoding("utf8");
106
97
  child.stdout?.on("data", (chunk) => this.#onStdout(chunk));
@@ -111,6 +102,20 @@ export class CodexAppServerTransport {
111
102
  child.on("error", (error) => this.#onExit(null, null, error));
112
103
  child.on("exit", (code, signal) => this.#onExit(code, signal));
113
104
  }
105
+ /**
106
+
107
+ * Resolve the child's runtime. An explicit `command` and world-home-less
108
+ * calls stay on the pure sync ladder; only a world home arms the lazy fetch
109
+ * of the pinned package (2026-10-06 ruling — never an install-time dep).
110
+ */
111
+ async #resolveRuntime(env) {
112
+ if (this.#options.command !== undefined || this.#options.worldHome === undefined) {
113
+ return resolveCodexRuntime({
114
+ env: this.#options.command === undefined ? env : { ...env, CODEX_BIN: this.#options.command },
115
+ });
116
+ }
117
+ return ensureCodexRuntime(this.#options.worldHome, createCodexRuntimeDeps({ env }));
118
+ }
114
119
  /** Send a request and await its result. Rejects on protocol error, timeout, or EOF. */
115
120
  request(method, params, options = {}) {
116
121
  if (this.#closed || this.#child === undefined) {
@@ -1,35 +1,4 @@
1
- /**
2
- * codex-launch.ts — how a provider reaches a `codex app-server` child
3
- * (2026-10-04 on-the-fly rework).
4
- *
5
- * The retired design put the provider in force by REWRITING the user's
6
- * `~/.codex/config.toml` before the child started. That made the user's native
7
- * home the switchboard: every switch was a disk write, only ever affected the
8
- * next child, and fought every other tool that owns the same file (cc-switch's
9
- * whole-config rewrites, the user's own hand edits).
10
- *
11
- * This module replaces it with process arguments. `codex app-server -c
12
- * <dotted.path>=<value>` overrides any `config.toml` value for THAT child
13
- * alone (the flag's own help text: "Override a configuration value that would
14
- * otherwise be loaded from `~/.codex/config.toml`. Use a dotted path."), and
15
- * `ThreadStartParams`/`ThreadResumeParams` accept the same object as `config`.
16
- * A provider therefore costs nothing on disk and is scoped to one process, which
17
- * is exactly what a per-session provider choice needs.
18
- *
19
- * Measured constraints this module encodes (see
20
- * `docs/test-reports/2026-10-03-codex-provider-model-switch-probe.md`):
21
- *
22
- * - a HYPHENATED id is fine in a dotted path (`model_providers.zhipu-cn.…`),
23
- * so native table names survive verbatim as provider ids;
24
- * - `model_catalog_json` is resolved against the process CWD when it arrives
25
- * as an override, so the path is always absolute;
26
- * - `env_key` is the one auth shape that never consults the auth manager, so a
27
- * stored API key rides the CHILD's environment under a synthetic variable
28
- * name. That is also what makes a keyed provider usable behind a ChatGPT
29
- * login, where `requires_openai_auth` is silently shadowed.
30
- *
31
- * @module agent-codex/codex-launch
32
- */
1
+ import { officialCatalogForLaunch } from "./codex-oauth.js";
33
2
  import { tomlString, writeProviderCatalog } from "./codex-native-config.js";
34
3
  /** The environment variable prefix for a key this adapter injects itself. */
35
4
  const SYNTHETIC_KEY_PREFIX = "SUPERD_CODEX_KEY_";
@@ -48,6 +17,15 @@ export function syntheticKeyName(providerId) {
48
17
  * @returns the argv/env additions and the provider id they pin.
49
18
  */
50
19
  export function providerLaunch(worldHome, codexHome, provider) {
20
+ if (provider.kind === "openai-oauth") {
21
+ const catalog = officialCatalogForLaunch(codexHome);
22
+ // Empty override is filtered to None by Codex, restoring its builtin
23
+ // endpoint without changing the user's native configuration.
24
+ const argv = ["-c", 'model_provider="openai"', "-c", 'openai_base_url=""'];
25
+ if (catalog !== undefined)
26
+ argv.push("-c", `model_catalog_json=${tomlString(catalog)}`);
27
+ return { argv, env: {}, runtimeProvider: "openai" };
28
+ }
51
29
  const table = `model_providers.${provider.id}`;
52
30
  const argv = [
53
31
  "-c", `${table}.name=${tomlString(provider.label)}`,
@@ -95,9 +95,10 @@ function slugOf(entry) {
95
95
  * that we do not already hold, then upserts `models` by slug. Each upserted
96
96
  * entry is a CLONE of `template` — an existing entry for the same slug if there
97
97
  * is one, else the caller's template, else the first entry either side supplied
98
- * — with only the identity fields overridden. The other ~35 keys are Codex's
99
- * and are copied through untouched; see the plan's D5 for why we do not try to
100
- * synthesize them.
98
+ * — with identity and declared context window overridden. The other metadata
99
+ * is copied through except `comp_hash`: this optional opaque compatibility
100
+ * identifier belongs to the template's model, not an unrelated provider model.
101
+ * See the plan's D5 for why we do not synthesize the remaining fields.
101
102
  */
102
103
  export function mergeModelCatalog(existing, absorbed, models, template) {
103
104
  // Both sides are a TEMPLATE POOL only — never output. What gets emitted is
@@ -132,6 +133,11 @@ export function mergeModelCatalog(existing, absorbed, models, template) {
132
133
  slug: model.id,
133
134
  display_name: model.displayName ?? model.id,
134
135
  };
136
+ // A copied OpenAI hash falsely triggers previous-model compaction on
137
+ // provider switches in Codex 0.160. Our provider model declarations do not
138
+ // carry verified hashes; remove stale generated values too. Context-limit
139
+ // compaction and its window/threshold metadata remain unchanged.
140
+ delete entry["comp_hash"];
135
141
  if (model.contextWindow !== undefined) {
136
142
  // Codex reads context_window and caps against max_context_window; a stale
137
143
  // template value must not silently cap a larger real window.
@@ -0,0 +1,153 @@
1
+ /** Native OAuth discovery. Tokens stay in Codex's own home; no network/spawn. */
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { readNativeSelection } from './codex-native-config.js';
5
+ import { readProviderStore } from './codex-providers.js';
6
+ export const OPENAI_OAUTH_ID = 'openai-oauth';
7
+ export function hasOpenAiOAuth(codexHome) {
8
+ try {
9
+ const value = JSON.parse(readFileSync(join(codexHome, 'auth.json'), 'utf8'));
10
+ const tokens = value?.tokens;
11
+ return value?.auth_mode === 'chatgpt' && tokens !== null && typeof tokens === 'object'
12
+ && [tokens.access_token, tokens.refresh_token].some(token => typeof token === 'string' && token.trim() !== '');
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function isRecord(value) {
19
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
20
+ }
21
+ function nonEmpty(value) {
22
+ return typeof value === 'string' && value.trim() !== '';
23
+ }
24
+ function oneOf(value, choices) {
25
+ return typeof value === 'string' && choices.includes(value);
26
+ }
27
+ /**
28
+ * The non-defaulted ModelInfo fields consumed by Codex, including its required
29
+ * nested metadata. Keep unknown native fields untouched; never repair or
30
+ * synthesize a catalog. Source: codex-rs/protocol/src/openai_models.rs (0.154 / 0.160).
31
+ */
32
+ function usableModel(value) {
33
+ if (!isRecord(value) || !nonEmpty(value['slug']) || typeof value['display_name'] !== 'string')
34
+ return false;
35
+ const reasoning = value['supported_reasoning_levels'];
36
+ const truncation = value['truncation_policy'];
37
+ const messages = value['model_messages'];
38
+ if (typeof value['base_instructions'] !== 'string' && !(isRecord(messages) && typeof messages['instructions_template'] === 'string'))
39
+ return false;
40
+ return Array.isArray(reasoning) && reasoning.every(level => isRecord(level) && nonEmpty(level['effort']) && typeof level['description'] === 'string')
41
+ && oneOf(value['shell_type'], ['unified_exec', 'default', 'local', 'shell_command', 'disabled'])
42
+ && oneOf(value['visibility'], ['list', 'hide', 'none'])
43
+ && typeof value['supported_in_api'] === 'boolean'
44
+ && Number.isSafeInteger(value['priority']) && Number(value['priority']) >= -2147483648 && Number(value['priority']) <= 2147483647
45
+ && typeof value['support_verbosity'] === 'boolean'
46
+ && isRecord(truncation) && oneOf(truncation['mode'], ['tokens', 'bytes']) && Number.isSafeInteger(truncation['limit'])
47
+ && Array.isArray(value['experimental_supported_tools']) && value['experimental_supported_tools'].every(tool => typeof tool === 'string');
48
+ }
49
+ /** One bad entry invalidates the file: Codex deserializes the whole catalog. */
50
+ function officialCatalogModels(codexHome) {
51
+ try {
52
+ const cache = JSON.parse(readFileSync(join(codexHome, 'models_cache.json'), 'utf8'));
53
+ if (!isRecord(cache))
54
+ return undefined;
55
+ const models = cache['models'];
56
+ if (Array.isArray(models) && models.length > 0 && models.every(usableModel))
57
+ return models;
58
+ }
59
+ catch { /* Missing/stale cache must not become a forced catalog override. */ }
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Conservatively detect configuration that would leak into an official child
64
+ * without a cache override. Inspect all tables: a profile can supply a catalog
65
+ * even when the root has none. Accept indentation, quoted and dotted keys. We
66
+ * only need to know whether a conflicting override exists, not parse its value.
67
+ */
68
+ function nativeAssignments(codexHome) {
69
+ let text;
70
+ try {
71
+ text = readFileSync(join(codexHome, 'config.toml'), 'utf8');
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ const assignments = [];
77
+ for (const line of text.split('\n')) {
78
+ const assignment = /^\s*((?:[\w-]+|"[^"\n]+"|'[^'\n]+')(?:\s*\.\s*(?:[\w-]+|"[^"\n]+"|'[^'\n]+'))*)\s*=\s*(.*)$/.exec(line);
79
+ if (assignment === null)
80
+ continue;
81
+ const key = assignment[1].split(/\s*\.\s*/).at(-1)?.replace(/^["']|["']$/g, '');
82
+ assignments.push({ key, value: assignment[2] });
83
+ }
84
+ return assignments;
85
+ }
86
+ function nativeCatalogConflict(codexHome) {
87
+ return nativeAssignments(codexHome).some(({ key, value }) => key === 'model_catalog_json' || key === 'profile'
88
+ || (key === 'model_provider' && !/^(?:"openai"|'openai')\s*(?:#.*)?$/.test(value)));
89
+ }
90
+ /** Official runtime cache only: never consume a third-party custom catalog. */
91
+ export function readOpenAiModels(codexHome) {
92
+ const models = [];
93
+ for (const raw of officialCatalogModels(codexHome) ?? []) {
94
+ if (raw['visibility'] !== 'list')
95
+ continue;
96
+ const slug = raw['slug'];
97
+ if (models.some(model => model.id === slug))
98
+ continue;
99
+ const displayName = raw['display_name'];
100
+ const contextWindow = raw['context_window'];
101
+ models.push({ id: slug,
102
+ ...(nonEmpty(displayName) ? { displayName } : {}),
103
+ ...(Number.isSafeInteger(contextWindow) && Number(contextWindow) > 0 ? { contextWindow: Number(contextWindow) } : {}),
104
+ });
105
+ }
106
+ // A different binary can leave a valid, older catalog in the shared home.
107
+ // Preserve the user's official configured choice as an identity, without
108
+ // inventing metadata or claiming that this cache/binary supports that model.
109
+ if (!nativeCatalogConflict(codexHome)) {
110
+ const native = readNativeSelection(codexHome);
111
+ if ((native.providerId === undefined || native.providerId === 'openai') && nonEmpty(native.model)
112
+ && !models.some(model => model.id === native.model))
113
+ models.push({ id: native.model });
114
+ }
115
+ return models;
116
+ }
117
+ /** Internal catalog shape; disk serialization stores just its native identity. */
118
+ export function openAiOAuthEntry(codexHome) {
119
+ return { id: OPENAI_OAUTH_ID, label: 'OpenAI OAuth', kind: 'openai-oauth', origin: 'native',
120
+ baseUrl: '', protocol: 'responses', auth: { kind: 'apiKey', value: '' }, models: readOpenAiModels(codexHome) };
121
+ }
122
+ export function readEffectiveProviderStore(worldHome, codexHome) {
123
+ const store = readProviderStore(worldHome);
124
+ const aliasEntries = store.providers.filter(entry => entry.id === OPENAI_OAUTH_ID);
125
+ const ambiguous = aliasEntries.length > 1;
126
+ const blocked = store.blockedNativeIds?.includes(OPENAI_OAUTH_ID) === true;
127
+ return { ...store, providers: store.providers.flatMap(entry => entry.id === OPENAI_OAUTH_ID && ambiguous ? [] : entry.kind === 'openai-oauth'
128
+ ? !blocked && hasOpenAiOAuth(codexHome) ? [openAiOAuthEntry(codexHome)] : [] : [entry]) };
129
+ }
130
+ /** A usable runtime-owned catalog path, excluding malformed/empty cache files. */
131
+ export function openAiCatalogPath(codexHome) {
132
+ return officialCatalogModels(codexHome) === undefined ? undefined : join(codexHome, 'models_cache.json');
133
+ }
134
+ /** Do not let an official child inherit a third-party model_catalog_json. */
135
+ export function officialCatalogForLaunch(codexHome) {
136
+ if (!hasOpenAiOAuth(codexHome))
137
+ throw new Error('OpenAI OAuth authentication is unavailable; sign in using Codex');
138
+ if (nativeAssignments(codexHome).some(({ key, value }) => key === 'forced_login_method' && !/^(?:"chatgpt"|'chatgpt')\s*(?:#.*)?$/.test(value))) {
139
+ throw new Error('OpenAI OAuth cannot use a conflicting forced_login_method; choose ChatGPT login in your Codex configuration');
140
+ }
141
+ // A forced catalog is static in Codex and disables version/identity/TTL
142
+ // refresh. Let the selected binary own discovery when native config is safe;
143
+ // only override a conflicting native catalog with the validated official one.
144
+ if (!nativeCatalogConflict(codexHome))
145
+ return undefined;
146
+ const catalog = openAiCatalogPath(codexHome);
147
+ if (catalog !== undefined)
148
+ return catalog;
149
+ const native = readNativeSelection(codexHome);
150
+ if (nativeCatalogConflict(codexHome) || (native.providerId !== undefined && native.providerId !== 'openai'))
151
+ throw new Error('OpenAI OAuth model cache unavailable; open Codex with the official provider to refresh its models');
152
+ return undefined;
153
+ }