super-dsh 0.1.9 → 0.1.10-e

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 (178) hide show
  1. package/README.md +25 -3
  2. package/agent-agy/cordis.patch.yml +7 -4
  3. package/agent-agy/dist/agent.d.ts +12 -11
  4. package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
  5. package/agent-agy/dist/agy-auth-gateway.js +233 -0
  6. package/agent-agy/dist/agy-auth.d.ts +153 -0
  7. package/agent-agy/dist/agy-auth.js +179 -0
  8. package/agent-agy/dist/agy-cli-client.d.ts +39 -9
  9. package/agent-agy/dist/agy-cli-client.js +82 -18
  10. package/agent-agy/dist/agy-store.d.ts +8 -0
  11. package/agent-agy/dist/index.d.ts +46 -16
  12. package/agent-agy/dist/index.js +82 -56
  13. package/agent-agy/dist/models.d.ts +70 -0
  14. package/agent-agy/dist/models.js +102 -3
  15. package/agent-agy/package.json +7 -5
  16. package/agent-agy-sdk/cordis.patch.yml +58 -0
  17. package/agent-agy-sdk/dist/adapter.d.ts +15 -0
  18. package/agent-agy-sdk/dist/adapter.js +42 -0
  19. package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
  20. package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
  21. package/agent-agy-sdk/dist/agent.d.ts +138 -0
  22. package/agent-agy-sdk/dist/agent.js +494 -0
  23. package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
  24. package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
  25. package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
  26. package/agent-agy-sdk/dist/agy-sessions.js +107 -0
  27. package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
  28. package/agent-agy-sdk/dist/agy-store.js +83 -0
  29. package/agent-agy-sdk/dist/inbox.d.ts +26 -0
  30. package/agent-agy-sdk/dist/inbox.js +53 -0
  31. package/agent-agy-sdk/dist/index.d.ts +93 -0
  32. package/agent-agy-sdk/dist/index.js +476 -0
  33. package/agent-agy-sdk/dist/models.d.ts +41 -0
  34. package/agent-agy-sdk/dist/models.js +74 -0
  35. package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
  36. package/agent-agy-sdk/dist/sdk-venv.js +173 -0
  37. package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
  38. package/agent-agy-sdk/dist/world-plugin.js +82 -0
  39. package/agent-agy-sdk/package.json +64 -0
  40. package/agent-claude/dist/adapter.js +106 -31
  41. package/agent-claude/dist/agent.js +154 -17
  42. package/agent-claude/dist/claude-client.js +84 -43
  43. package/agent-claude/dist/claude-launch.js +101 -0
  44. package/agent-claude/dist/claude-native-config.js +169 -0
  45. package/agent-claude/dist/claude-provider-gateway.js +340 -0
  46. package/agent-claude/dist/claude-providers.js +391 -0
  47. package/agent-claude/dist/claude-route.js +152 -0
  48. package/agent-claude/dist/index.js +214 -37
  49. package/agent-claude/dist/models.js +14 -0
  50. package/agent-claude/dist/sdk-runtime.js +320 -0
  51. package/agent-claude/package.json +6 -4
  52. package/agent-codex/README.md +141 -36
  53. package/agent-codex/cordis.patch.yml +14 -10
  54. package/agent-codex/dist/adapter.js +64 -28
  55. package/agent-codex/dist/agent.js +255 -81
  56. package/agent-codex/dist/app-server-approvals.js +165 -0
  57. package/agent-codex/dist/app-server-client.js +723 -0
  58. package/agent-codex/dist/app-server-events.js +432 -0
  59. package/agent-codex/dist/app-server-protocol.js +95 -0
  60. package/agent-codex/dist/app-server-wire.js +299 -0
  61. package/agent-codex/dist/codex-launch.js +78 -0
  62. package/agent-codex/dist/codex-native-config.js +392 -0
  63. package/agent-codex/dist/codex-provider-gateway.js +402 -0
  64. package/agent-codex/dist/codex-providers.js +376 -0
  65. package/agent-codex/dist/codex-route.js +86 -0
  66. package/agent-codex/dist/index.js +256 -115
  67. package/agent-codex/dist/permission.js +75 -1
  68. package/agent-codex/package.json +1 -2
  69. package/agent-codex-sdk/README.md +79 -0
  70. package/agent-codex-sdk/cordis.patch.yml +81 -0
  71. package/agent-codex-sdk/dist/adapter.js +91 -0
  72. package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
  73. package/agent-codex-sdk/dist/agent.js +1085 -0
  74. package/agent-codex-sdk/dist/codex-native-config.js +392 -0
  75. package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
  76. package/agent-codex-sdk/dist/codex-providers.js +376 -0
  77. package/agent-codex-sdk/dist/codex-route.js +86 -0
  78. package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
  79. package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
  80. package/agent-codex-sdk/dist/codex-store.js +180 -0
  81. package/agent-codex-sdk/dist/inbox.js +53 -0
  82. package/agent-codex-sdk/dist/index.js +739 -0
  83. package/agent-codex-sdk/dist/permission.js +71 -0
  84. package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
  85. package/agent-codex-sdk/dist/session-map.js +167 -0
  86. package/agent-codex-sdk/dist/world-plugin.js +93 -0
  87. package/agent-codex-sdk/package.json +79 -0
  88. package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
  89. package/agent-omp/README.md +72 -55
  90. package/agent-omp/dist/agent.js +36 -11
  91. package/agent-omp/dist/index.js +92 -52
  92. package/agent-omp/dist/lazy-rpc.js +11 -4
  93. package/agent-omp/dist/omp-cli.js +114 -97
  94. package/agent-omp/dist/omp-store.js +12 -4
  95. package/agent-omp/dist/permission.js +70 -5
  96. package/agent-omp/dist/rpc-client.js +409 -0
  97. package/agent-omp/dist/world-plugin.js +36 -3
  98. package/agent-omp/package.json +2 -6
  99. package/agent-omp-sdk/README.md +103 -0
  100. package/agent-omp-sdk/cordis.patch.yml +108 -0
  101. package/agent-omp-sdk/dist/adapter.d.ts +21 -0
  102. package/agent-omp-sdk/dist/adapter.js +36 -0
  103. package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
  104. package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
  105. package/agent-omp-sdk/dist/agent.d.ts +64 -0
  106. package/agent-omp-sdk/dist/agent.js +1178 -0
  107. package/agent-omp-sdk/dist/discovery.d.ts +57 -0
  108. package/agent-omp-sdk/dist/discovery.js +116 -0
  109. package/agent-omp-sdk/dist/inbox.d.ts +28 -0
  110. package/agent-omp-sdk/dist/inbox.js +53 -0
  111. package/agent-omp-sdk/dist/index.d.ts +60 -0
  112. package/agent-omp-sdk/dist/index.js +632 -0
  113. package/agent-omp-sdk/dist/knobs.d.ts +34 -0
  114. package/agent-omp-sdk/dist/knobs.js +50 -0
  115. package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
  116. package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
  117. package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
  118. package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
  119. package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
  120. package/agent-omp-sdk/dist/mobile-boot.js +38 -0
  121. package/agent-omp-sdk/dist/models.d.ts +39 -0
  122. package/agent-omp-sdk/dist/models.js +299 -0
  123. package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
  124. package/agent-omp-sdk/dist/omp-cli.js +164 -0
  125. package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
  126. package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
  127. package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
  128. package/agent-omp-sdk/dist/omp-executable.js +30 -0
  129. package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
  130. package/agent-omp-sdk/dist/omp-store.js +526 -0
  131. package/agent-omp-sdk/dist/pairing.d.ts +7 -0
  132. package/agent-omp-sdk/dist/pairing.js +45 -0
  133. package/agent-omp-sdk/dist/permission.d.ts +138 -0
  134. package/agent-omp-sdk/dist/permission.js +161 -0
  135. package/agent-omp-sdk/dist/protocol.d.ts +189 -0
  136. package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
  137. package/agent-omp-sdk/dist/rpc-types.js +1 -0
  138. package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
  139. package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
  140. package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
  141. package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
  142. package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
  143. package/agent-omp-sdk/dist/sidecar-client.js +197 -0
  144. package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
  145. package/agent-omp-sdk/dist/store/adapter.js +47 -0
  146. package/agent-omp-sdk/dist/store/db.d.ts +70 -0
  147. package/agent-omp-sdk/dist/store/db.js +210 -0
  148. package/agent-omp-sdk/dist/store/index.d.ts +23 -0
  149. package/agent-omp-sdk/dist/store/index.js +59 -0
  150. package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
  151. package/agent-omp-sdk/dist/store/reconcile.js +51 -0
  152. package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
  153. package/agent-omp-sdk/dist/store/schema.js +40 -0
  154. package/agent-omp-sdk/dist/transport.d.ts +49 -0
  155. package/agent-omp-sdk/dist/transport.js +35 -0
  156. package/agent-omp-sdk/dist/world-home.d.ts +4 -0
  157. package/agent-omp-sdk/dist/world-home.js +23 -0
  158. package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
  159. package/agent-omp-sdk/dist/world-plugin.js +137 -0
  160. package/agent-omp-sdk/package.json +87 -0
  161. package/client/index.js +1204 -93
  162. package/cordis.patch.yml +64 -0
  163. package/package.json +17 -8
  164. package/superd-hub/dist/client/agent-settings.d.ts +210 -0
  165. package/superd-hub/dist/client/agent-settings.js +300 -0
  166. package/superd-hub/dist/client/roster.js +6 -0
  167. package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
  168. package/superd-hub/dist/client/runtime-settings-api.js +137 -0
  169. package/superd-hub/dist/client/switch.d.ts +165 -0
  170. package/superd-hub/dist/client/switch.js +295 -0
  171. package/superd-hub/lib/client/index.js +1204 -93
  172. package/agent-codex/dist/models.js +0 -152
  173. package/agent-omp/dist/sidecar-client.js +0 -195
  174. /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
  175. /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
  176. /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
  177. /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
  178. /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
@@ -0,0 +1,376 @@
1
+ /**
2
+ * codex-providers.ts — the adapter-owned multi-provider store behind the Hub's
3
+ * Codex settings panel (2026-10-03 user ruling; reworked 2026-10-04).
4
+ *
5
+ * Codex is a single-provider harness: its own `config.toml` can name exactly
6
+ * ONE `model_provider`, so the LIST of configured providers has nowhere to live
7
+ * in Codex's home, and this module is where it lives instead:
8
+ * `<worldHome>/codex-providers.json` under the world's DSH home
9
+ * (`<dshHome>/agents/codex/`), the location dev-rules §5/§13 already assigns to
10
+ * the adapter's own DSH state.
11
+ *
12
+ * This file is the CANONICAL provider list. The native `config.toml` is a
13
+ * SOURCE, not a peer: `syncNativeProviders` folds whatever it declares into
14
+ * this list, deduplicated by endpoint URL (the only identity a hand-written
15
+ * codex config reliably carries), and never deletes anything. There is no
16
+ * "selected" provider any more — which provider a session runs on is DSH's own
17
+ * per-session `model/selection`, and the native home is written by nobody.
18
+ *
19
+ * The file carries API keys, so it is written 0600 with the sibling
20
+ * `.credentials.yaml` posture, and reads are fail-soft: a corrupt or partial
21
+ * store must degrade to "no providers yet", never take the world down. A
22
+ * provider entry that does not survive validation is DROPPED rather than
23
+ * poisoning the whole list.
24
+ *
25
+ * @module agent-codex/codex-providers
26
+ */
27
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
28
+ import { dirname, join } from "node:path";
29
+ /**
30
+ * A provider id becomes a TOML table name and is interpolated into the native
31
+ * config, so the alphabet is deliberately narrow: lowercase alphanumerics and
32
+ * interior hyphens. (Covers `zai-plan`, `deepseek`, `openai`; rejects spaces,
33
+ * dots, quotes and anything that would need TOML escaping.)
34
+ */
35
+ const PROVIDER_ID = /^[a-z0-9][a-z0-9-]*$/;
36
+ /** Longest generated id: the slug part plus its 7-character discriminator. */
37
+ const MAX_ID_LENGTH = 48;
38
+ /** Whether `value` may be used as a provider id. */
39
+ export function isProviderId(value) {
40
+ return typeof value === "string" && PROVIDER_ID.test(value);
41
+ }
42
+ /** Whether `value` names a wire protocol this adapter understands. */
43
+ export function isWireProtocol(value) {
44
+ return value === "responses" || value === "chat";
45
+ }
46
+ /** Where the store lives: `<worldHome>/codex-providers.json`. */
47
+ export function providerStorePath(worldHome) {
48
+ return join(worldHome, "codex-providers.json");
49
+ }
50
+ /** The empty store — also the fail-soft answer for every unreadable input. */
51
+ function emptyStore() {
52
+ return { version: 1, providers: [] };
53
+ }
54
+ function str(value) {
55
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
56
+ }
57
+ function auth(value) {
58
+ if (value === null || typeof value !== "object")
59
+ return undefined;
60
+ const record = value;
61
+ if (record["kind"] === "apiKey") {
62
+ // An EMPTY value is a real state, not a malformed entry: it means "this
63
+ // provider is keyed by auth.json and Super D does not hold a copy", which is
64
+ // how a provider seeded from the user's own config arrives. Refusing it would
65
+ // make the user's currently-working provider unrepresentable.
66
+ const secret = record["value"];
67
+ return typeof secret === "string" ? { kind: "apiKey", value: secret } : undefined;
68
+ }
69
+ if (record["kind"] === "env") {
70
+ const name = str(record["name"]);
71
+ return name === undefined ? undefined : { kind: "env", name };
72
+ }
73
+ return undefined;
74
+ }
75
+ function model(value) {
76
+ if (value === null || typeof value !== "object")
77
+ return undefined;
78
+ const record = value;
79
+ const id = str(record["id"]);
80
+ if (id === undefined)
81
+ return undefined;
82
+ const displayName = str(record["displayName"]);
83
+ const contextWindow = record["contextWindow"];
84
+ return {
85
+ id,
86
+ ...(displayName === undefined ? {} : { displayName }),
87
+ ...(typeof contextWindow === "number" && Number.isSafeInteger(contextWindow) && contextWindow > 0
88
+ ? { contextWindow }
89
+ : {}),
90
+ };
91
+ }
92
+ /**
93
+ * Validate one raw entry. Returns `undefined` for anything unusable — an
94
+ * unknown protocol, an id that cannot be a TOML table, a missing endpoint — so
95
+ * the caller can drop it and keep the rest. An empty `models` list is VALID
96
+ * (see {@link CodexProviderEntry.models}).
97
+ */
98
+ export function providerEntry(value) {
99
+ if (value === null || typeof value !== "object")
100
+ return undefined;
101
+ const record = value;
102
+ const id = record["id"];
103
+ const label = str(record["label"]);
104
+ const baseUrl = str(record["baseUrl"]);
105
+ const protocol = record["protocol"];
106
+ const credentials = auth(record["auth"]);
107
+ if (!isProviderId(id) || label === undefined || baseUrl === undefined || !isWireProtocol(protocol) || credentials === undefined) {
108
+ return undefined;
109
+ }
110
+ const rawModels = record["models"];
111
+ const models = (Array.isArray(rawModels) ? rawModels : [])
112
+ .map(model)
113
+ .filter((entry) => entry !== undefined);
114
+ const origin = record["origin"] === "native" ? "native" : "user";
115
+ return { id, label, baseUrl, protocol, auth: credentials, models, origin };
116
+ }
117
+ /**
118
+ * Read the store. Fail-soft by construction: a missing file, unparsable JSON,
119
+ * a wrong top-level shape, or individual bad entries all degrade toward an
120
+ * empty (or shorter) list rather than throwing.
121
+ *
122
+ * A `selected` member written by an earlier build is ignored on purpose — the
123
+ * global selection was retired in favour of DSH's per-session `model/selection`.
124
+ */
125
+ export function readProviderStore(worldHome) {
126
+ const path = providerStorePath(worldHome);
127
+ if (!existsSync(path))
128
+ return emptyStore();
129
+ let parsed;
130
+ try {
131
+ parsed = JSON.parse(readFileSync(path, "utf8"));
132
+ }
133
+ catch {
134
+ return emptyStore();
135
+ }
136
+ if (parsed === null || typeof parsed !== "object")
137
+ return emptyStore();
138
+ const record = parsed;
139
+ if (!Array.isArray(record["providers"]))
140
+ return emptyStore();
141
+ const providers = record["providers"]
142
+ .map(providerEntry)
143
+ .filter((entry) => entry !== undefined);
144
+ const rawRemoved = record["removed"];
145
+ const removed = (Array.isArray(rawRemoved) ? rawRemoved : [])
146
+ .filter((value) => typeof value === "string" && value.trim() !== "");
147
+ return { version: 1, providers, ...(removed.length === 0 ? {} : { removed }) };
148
+ }
149
+ /**
150
+ * Persist the store atomically (tmp + rename) and 0600 — the file holds API
151
+ * keys in the clear, exactly like the sibling `.credentials.yaml`, so it must
152
+ * never be group- or world-readable even for the instant before a rewrite.
153
+ */
154
+ export function writeProviderStore(worldHome, store) {
155
+ const path = providerStorePath(worldHome);
156
+ mkdirSync(dirname(path), { recursive: true });
157
+ const temp = `${path}.tmp-${process.pid}`;
158
+ writeFileSync(temp, `${JSON.stringify(store, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
159
+ renameSync(temp, path);
160
+ }
161
+ /**
162
+ * Insert or replace one provider by id, preserving list order.
163
+ *
164
+ * An explicit add also clears the endpoint's removal tombstone: re-adding a
165
+ * provider is the user overruling their own earlier removal, and leaving the
166
+ * tombstone would make the next sync skip it again.
167
+ */
168
+ export function upsertProvider(store, entry) {
169
+ const at = store.providers.findIndex((existing) => existing.id === entry.id);
170
+ const providers = at === -1
171
+ ? [...store.providers, entry]
172
+ : store.providers.map((existing, index) => (index === at ? entry : existing));
173
+ const key = endpointKey(entry.baseUrl);
174
+ const removed = (store.removed ?? []).filter((candidate) => candidate !== key);
175
+ // `removed` must be CLEARED, not merely omitted: spreading the old store would
176
+ // carry the tombstone forward, and the next sync would keep skipping the
177
+ // endpoint the user just re-added.
178
+ const { removed: _stale, ...rest } = store;
179
+ return { ...rest, providers, ...(removed.length === 0 ? {} : { removed }) };
180
+ }
181
+ /**
182
+ * Drop one provider, and TOMBSTONE its endpoint so the reconcile cannot bring
183
+ * it back: the user's own `config.toml` still declares it, and a list editor
184
+ * whose delete button undoes itself every ten minutes is worse than no button.
185
+ *
186
+ * Removing from this list is still not "unconfiguring Codex": nothing native is
187
+ * touched, and the user's own CLI keeps running whatever their file says.
188
+ */
189
+ export function removeProvider(store, id) {
190
+ const entry = store.providers.find((candidate) => candidate.id === id);
191
+ const providers = store.providers.filter((candidate) => candidate.id !== id);
192
+ if (entry === undefined)
193
+ return { ...store, providers };
194
+ const key = endpointKey(entry.baseUrl);
195
+ const removed = store.removed?.includes(key) === true ? store.removed : [...store.removed ?? [], key];
196
+ return { ...store, providers, removed };
197
+ }
198
+ /**
199
+ * The comparison key for "the same endpoint". Codex itself treats two provider
200
+ * tables with the same `base_url` as the same service, and a hand-written
201
+ * config carries no other identity worth trusting — so the endpoint IS the
202
+ * dedupe key. Case, a trailing slash, and the default port are noise.
203
+ *
204
+ * @param baseUrl - a provider's `base_url` (any shape; unparsable input is compared verbatim).
205
+ * @returns a stable key.
206
+ */
207
+ export function endpointKey(baseUrl) {
208
+ const trimmed = baseUrl.trim();
209
+ let parsed;
210
+ try {
211
+ parsed = new URL(trimmed);
212
+ }
213
+ catch {
214
+ return trimmed.toLowerCase();
215
+ }
216
+ const path = parsed.pathname.replace(/\/+$/, "");
217
+ const port = parsed.port === "" || (parsed.protocol === "https:" && parsed.port === "443")
218
+ || (parsed.protocol === "http:" && parsed.port === "80")
219
+ ? ""
220
+ : `:${parsed.port}`;
221
+ return `${parsed.protocol}//${parsed.hostname.toLowerCase()}${port}${path}`;
222
+ }
223
+ /** Reduce arbitrary text to the provider-id alphabet, collapsing runs of junk. */
224
+ function slugify(value) {
225
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
226
+ return slug === "" ? "provider" : slug.slice(0, MAX_ID_LENGTH - 8);
227
+ }
228
+ /** A short, stable discriminator so two endpoints sharing a host stay distinct. */
229
+ function discriminator(value) {
230
+ let hash = 0x811c9dc5;
231
+ for (let index = 0; index < value.length; index += 1) {
232
+ hash ^= value.charCodeAt(index);
233
+ hash = Math.imul(hash, 0x01000193) >>> 0;
234
+ }
235
+ return hash.toString(36).padStart(6, "0").slice(-6);
236
+ }
237
+ /**
238
+ * The provider id for an endpoint that arrives without a usable one. Codex's
239
+ * own `config.toml` tables are already valid ids and are preferred by the
240
+ * caller; this is the fallback for a UI-added provider and for a native table
241
+ * name that cannot be a TOML key.
242
+ *
243
+ * @param baseUrl - the provider's endpoint.
244
+ * @returns a slug that satisfies {@link isProviderId}.
245
+ */
246
+ export function providerIdFromEndpoint(baseUrl) {
247
+ const key = endpointKey(baseUrl);
248
+ let host = "provider";
249
+ let path = "";
250
+ try {
251
+ const parsed = new URL(key);
252
+ host = parsed.hostname;
253
+ path = parsed.pathname;
254
+ }
255
+ catch {
256
+ // keep the defaults
257
+ }
258
+ return `${slugify(`${host}${path}`)}-${discriminator(key)}`;
259
+ }
260
+ /**
261
+ * Make `wanted` usable and unique within `taken`.
262
+ *
263
+ * @param wanted - the preferred id (a native table name, or a generated slug).
264
+ * @param taken - ids already claimed by other entries.
265
+ * @returns a valid id not present in `taken`.
266
+ */
267
+ export function uniqueProviderId(wanted, taken) {
268
+ const used = new Set(taken);
269
+ const base = isProviderId(wanted) ? wanted : slugify(wanted);
270
+ if (!used.has(base))
271
+ return base;
272
+ const salted = `${base.slice(0, MAX_ID_LENGTH - 7)}-${discriminator(wanted)}`;
273
+ if (!used.has(salted))
274
+ return salted;
275
+ for (let index = 2;; index += 1) {
276
+ const candidate = `${base.slice(0, MAX_ID_LENGTH - 4)}-${index}`;
277
+ if (!used.has(candidate))
278
+ return candidate;
279
+ }
280
+ }
281
+ /** Append the models of `incoming` that `existing` does not already carry. */
282
+ function unionModels(existing, incoming) {
283
+ const known = new Set(existing.map((entry) => entry.id));
284
+ const additions = incoming
285
+ .filter((id) => id !== "" && !known.has(id))
286
+ .map((id) => ({ id }));
287
+ return additions.length === 0 ? existing : [...existing, ...additions];
288
+ }
289
+ /**
290
+ * Structural auth equality.
291
+ *
292
+ * Reference equality is wrong here: a native sync re-parses the config on every
293
+ * tick, so the incoming auth object is a FRESH one each time and `!==` would
294
+ * report a change forever. That is not a cosmetic difference — it makes the
295
+ * "only write when something moved" contract fail, so every scheduled reconcile
296
+ * would rewrite a 0600 secrets file (observed: `updated=["deepseek","zhipu-cn"]`
297
+ * on an unchanged config).
298
+ */
299
+ function sameAuth(left, right) {
300
+ if (left.kind === "env")
301
+ return right.kind === "env" && left.name === right.name;
302
+ return right.kind === "apiKey" && left.value === right.value;
303
+ }
304
+ /**
305
+ * Fold the providers a native `config.toml` declares into the canonical store.
306
+ *
307
+ * Identity is the endpoint (see {@link endpointKey}); a native provider whose id
308
+ * or endpoint already matches an entry refreshes that entry's endpoint,
309
+ * protocol, model list and — only when the entry holds no secret of its own —
310
+ * its auth shape. Anything unmatched is appended, labelled with its endpoint
311
+ * URL, because that is the only name a hand-written codex config gives it.
312
+ * Nothing is ever deleted here: a provider dropped from `config.toml` may still
313
+ * be one the user uses through Super D. The one exception runs the other way —
314
+ * an endpoint the user REMOVED in the panel is skipped, so their removal is not
315
+ * undone by the next reconcile.
316
+ *
317
+ * @param store - the canonical list.
318
+ * @param imported - providers parsed from the native config, in file order.
319
+ * @returns the new store plus which ids were added and which were refreshed.
320
+ */
321
+ export function syncNativeProviders(store, imported) {
322
+ const providers = [...store.providers];
323
+ const added = [];
324
+ const updated = [];
325
+ for (const incoming of imported) {
326
+ const baseUrl = incoming.baseUrl.trim();
327
+ if (baseUrl === "")
328
+ continue;
329
+ const key = endpointKey(baseUrl);
330
+ if (store.removed?.includes(key) === true)
331
+ continue;
332
+ const wantedId = incoming.nativeId !== undefined && isProviderId(incoming.nativeId) ? incoming.nativeId : undefined;
333
+ const at = providers.findIndex((entry) => (wantedId !== undefined && entry.id === wantedId) || endpointKey(entry.baseUrl) === key);
334
+ if (at === -1) {
335
+ const id = uniqueProviderId(wantedId ?? providerIdFromEndpoint(baseUrl), providers.map((entry) => entry.id));
336
+ providers.push({
337
+ id,
338
+ label: baseUrl,
339
+ baseUrl,
340
+ protocol: incoming.protocol,
341
+ auth: incoming.auth,
342
+ models: unionModels([], incoming.models),
343
+ origin: "native",
344
+ });
345
+ added.push(id);
346
+ continue;
347
+ }
348
+ const entry = providers[at];
349
+ const models = unionModels(entry.models, incoming.models);
350
+ // The native table is the user's own statement about how codex should
351
+ // authenticate, but a secret Super D already holds must not be dropped in
352
+ // favour of "look in auth.json" — an empty apiKey value is the only shape
353
+ // that defers to the runtime, so only that one follows the native file.
354
+ const adoptAuth = entry.auth.kind === "env" || entry.auth.value === "";
355
+ const next = {
356
+ ...entry,
357
+ baseUrl,
358
+ protocol: incoming.protocol,
359
+ ...(adoptAuth ? { auth: incoming.auth } : {}),
360
+ models,
361
+ };
362
+ const changed = next.baseUrl !== entry.baseUrl
363
+ || next.protocol !== entry.protocol
364
+ || next.models !== entry.models
365
+ || !sameAuth(next.auth, entry.auth);
366
+ if (changed) {
367
+ providers[at] = next;
368
+ updated.push(entry.id);
369
+ }
370
+ }
371
+ const removed = store.removed ?? [];
372
+ const next = removed.length === 0
373
+ ? { version: 1, providers }
374
+ : { version: 1, providers, removed };
375
+ return { store: next, added, updated };
376
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * codex-route.ts — which provider/model a Codex turn must run
3
+ * (2026-10-04 multi-provider rework).
4
+ *
5
+ * This is the whole of Super D's model-selection policy, and the point is that
6
+ * there is almost none: the precedence below is the harness's own
7
+ * (`sessionController.selectionFor(agent).current`), so a session's route is
8
+ * decided by DSH exactly as it would be for a native agent, and Super D only
9
+ * has to read it.
10
+ *
11
+ * pending the durable `modelSelection` projection's `pending` — a switch
12
+ * made through `session/selectModel` that no `request/header` has
13
+ * retired yet. This is what makes a mid-turn switch land on the NEXT
14
+ * turn, for free: the harness records the intent and the next
15
+ * request consumes it.
16
+ * logged the last `request/header` snapshot — what the session ACTUALLY ran
17
+ * last. It is also what lets a resumed session name the provider
18
+ * that produced its transcript.
19
+ * default `ctx.agentDefaultModel` — the deployment default for a session
20
+ * that has never run.
21
+ * fallback the agent's own options, which the factory built from the user's
22
+ * native `config.toml`.
23
+ *
24
+ * Extracted from `agent.ts` so the precedence can be pinned without standing up
25
+ * a Session, a Context and a client: it is the one piece of this feature whose
26
+ * mistakes are silent.
27
+ *
28
+ * @module agent-codex/codex-route
29
+ */
30
+ /**
31
+ * Read a route out of one candidate value.
32
+ *
33
+ * Every candidate arrives as untyped projection/config data, so this validates
34
+ * structurally and answers `undefined` for anything incomplete. A half-filled
35
+ * route must never win the precedence: falling through to a complete one is
36
+ * always better than launching a child with an empty model.
37
+ *
38
+ * @param value - a candidate route (a `ModelSelection`-shaped object, or junk).
39
+ * @returns the route, or `undefined` when it does not name both halves.
40
+ */
41
+ export function routeOf(value) {
42
+ if (value === null || typeof value !== "object" || Array.isArray(value))
43
+ return undefined;
44
+ const record = value;
45
+ const provider = record["provider"];
46
+ const model = record["model"];
47
+ if (typeof provider !== "string" || provider === "" || typeof model !== "string" || model === "")
48
+ return undefined;
49
+ const reasoningEffort = record["reasoningEffort"];
50
+ return {
51
+ provider,
52
+ model,
53
+ ...(typeof reasoningEffort === "string" && reasoningEffort !== "" ? { reasoningEffort } : {}),
54
+ };
55
+ }
56
+ /**
57
+ * Resolve the route for the next request by the harness's own precedence.
58
+ *
59
+ * @param candidates - the candidates in descending priority: `pending`,
60
+ * `logged`, `preferred`, `fallback`. `preferred` is the harness default
61
+ * (`agentDefaultModel`); `fallback` is the factory's own options.
62
+ * @returns the first complete route, or `undefined` when none is usable.
63
+ */
64
+ export function resolveRoute(candidates) {
65
+ return routeOf(candidates.pending)
66
+ ?? routeOf(candidates.logged)
67
+ ?? routeOf(candidates.preferred)
68
+ ?? routeOf(candidates.fallback);
69
+ }
70
+ /**
71
+ * The route a brand-new session's log is SEEDED with.
72
+ *
73
+ * The single argument is deliberate. The factory's reconciled `AgentOptions` is
74
+ * the only acceptable source, because it is already ONE pair: a first attempt at
75
+ * this built the seed from the native provider id and the deployment default
76
+ * model independently, which produced `zhipu-cn/deepseek-flash` — a route no
77
+ * provider serves, and which had to be caught by reading a live session's
78
+ * projection rather than by any unit test. Taking one object makes that mistake
79
+ * unrepresentable.
80
+ *
81
+ * @param options - the factory's reconciled agent options.
82
+ * @returns the seed, or `undefined` when the pair is incomplete.
83
+ */
84
+ export function seedRoute(options) {
85
+ return routeOf(options);
86
+ }