super-dsh 0.1.10-e → 0.1.11

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 (38) hide show
  1. package/README.md +18 -2
  2. package/agent-agy/dist/agent.js +19 -1
  3. package/agent-claude/dist/agent.js +23 -5
  4. package/agent-codex/README.md +18 -6
  5. package/agent-codex/dist/adapter.js +3 -2
  6. package/agent-codex/dist/agent.js +41 -7
  7. package/agent-codex/dist/app-server-client.js +7 -1
  8. package/agent-codex/dist/app-server-wire.js +10 -22
  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.js +120 -0
  15. package/agent-codex/dist/index.js +18 -11
  16. package/agent-codex/package.json +1 -1
  17. package/agent-codex-sdk/dist/adapter.js +3 -2
  18. package/agent-codex-sdk/dist/agent.js +35 -1
  19. package/agent-codex-sdk/dist/codex-native-config.js +9 -3
  20. package/agent-codex-sdk/dist/codex-oauth.js +153 -0
  21. package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
  22. package/agent-codex-sdk/dist/codex-providers.js +20 -7
  23. package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
  24. package/agent-codex-sdk/dist/index.js +18 -11
  25. package/agent-codex-sdk/dist/sdk-runtime.js +2 -2
  26. package/agent-hermes/dist/agent.js +46 -2
  27. package/agent-hermes/dist/hermes-client.js +49 -1
  28. package/agent-omp/dist/agent.js +17 -4
  29. package/agent-omp-sdk/dist/agent.d.ts +11 -0
  30. package/agent-omp-sdk/dist/agent.js +17 -4
  31. package/agent-pi/dist/agent.js +33 -1
  32. package/client/index.js +10 -7
  33. package/package.json +2 -2
  34. package/superd-hub/dist/client/agent-settings.d.ts +1 -1
  35. package/superd-hub/dist/client/agent-settings.js +2 -2
  36. package/superd-hub/dist/world-mount.d.ts +6 -5
  37. package/superd-hub/dist/world-mount.js +6 -6
  38. package/superd-hub/lib/client/index.js +10 -7
@@ -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
+ }
@@ -1,34 +1,3 @@
1
- /**
2
- * codex-provider-gateway.ts — the adapter-owned Remote behind the Hub's Codex
3
- * settings panel (2026-10-03 user ruling; reworked 2026-10-04).
4
- *
5
- * Why a Remote and not a hub route: the provider list is ADAPTER config
6
- * (dev-rules §12), so the adapter owns it and the hub only forwards.
7
- * `TypertRemoteService` + `@Remote` is the whole mechanism — standard (TC39)
8
- * method decorators recorded on the class prototype, read back at dispatch time
9
- * by the gateway's source-claim scan. No codegen, no compiler-injected metadata,
10
- * which is why a plain `tsc` adapter build can ship one.
11
- *
12
- * Wire contract (`superd-hub/src/client/runtime-settings-api.ts` builds each
13
- * envelope; the hub carrier forwards `envelope.method` verbatim):
14
- *
15
- * request POST /hub/api?agent=codex
16
- * { type:'client-request', rpcId, method:'codexProviders/<op>',
17
- * payload: { args: { request?: … } } }
18
- * response { type:'server-response', rpcId, result:{ ok:true, value:<view> } }
19
- *
20
- * There is no `select`: which provider a SESSION runs on is DSH's own
21
- * per-session `model/selection`, chosen from the harness's model selector. This
22
- * panel is a list editor — add, edit, remove — plus the reconcile with the
23
- * user's `config.toml` that {@link syncFromNative} performs.
24
- *
25
- * The class is a delegation shell on purpose: every operation is a plain
26
- * function over (worldHome, codexHome), so the entire contract — the key
27
- * privacy rule, the validation, the sync, the view projection — is pinnable
28
- * under `node --test` with no Cordis Context and no real home.
29
- *
30
- * @module agent-codex/codex-provider-gateway
31
- */
32
1
  var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
33
2
  var useValue = arguments.length > 2;
34
3
  for (var i = 0; i < initializers.length; i++) {
@@ -63,6 +32,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
63
32
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
64
33
  done = true;
65
34
  };
35
+ import { OPENAI_OAUTH_ID, hasOpenAiOAuth, openAiOAuthEntry } from "./codex-oauth.js";
66
36
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
67
37
  import { isProviderId, isWireProtocol, providerStorePath, readProviderStore, removeProvider, syncNativeProviders, upsertProvider, writeProviderStore, } from "./codex-providers.js";
68
38
  import { providerCatalogDir, readNativeProviders, readNativeSelection, } from "./codex-native-config.js";
@@ -134,6 +104,8 @@ function keyAvailable(codexHome, entry) {
134
104
  return nonEmpty(process.env[entry.auth.name]) !== undefined;
135
105
  }
136
106
  function project(codexHome, entry) {
107
+ if (entry.kind === "openai-oauth")
108
+ return { id: entry.id, label: entry.label, protocol: "responses", models: openAiOAuthEntry(codexHome).models, authKind: "oauth", hasKey: hasOpenAiOAuth(codexHome) };
137
109
  return {
138
110
  id: entry.id,
139
111
  label: entry.label,
@@ -172,10 +144,22 @@ export function syncFromNative(worldHome, codexHome) {
172
144
  const store = readProviderStore(worldHome);
173
145
  const imported = readNativeProviders(codexHome).map(toImport);
174
146
  const result = syncNativeProviders(store, imported);
175
- if (result.added.length > 0 || result.updated.length > 0) {
176
- writeProviderStore(worldHome, result.store);
177
- }
178
- return result;
147
+ const legacyAlias = result.store.providers.some(entry => entry.id === OPENAI_OAUTH_ID && entry.kind !== "openai-oauth");
148
+ const blockedNativeIds = [...new Set([...(result.store.blockedNativeIds ?? []), ...(legacyAlias ? [OPENAI_OAUTH_ID] : [])])];
149
+ const oauth = hasOpenAiOAuth(codexHome) && !blockedNativeIds.includes(OPENAI_OAUTH_ID);
150
+ const existed = store.providers.some(entry => entry.kind === "openai-oauth");
151
+ // Keep collided identities on disk so ambiguous old routes fail closed. They
152
+ // are quarantined by readEffectiveProviderStore, not silently repurposed.
153
+ const providers = result.store.providers.flatMap(entry => entry.kind === "openai-oauth"
154
+ ? legacyAlias || oauth ? [openAiOAuthEntry(codexHome)] : [] : [entry]);
155
+ if (oauth && !existed)
156
+ providers.push(openAiOAuthEntry(codexHome));
157
+ const effective = { ...result.store, providers, ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
158
+ const blockedChanged = JSON.stringify(blockedNativeIds) !== JSON.stringify(store.blockedNativeIds ?? []);
159
+ const identityChanged = !legacyAlias && oauth !== existed;
160
+ if (result.added.length > 0 || result.updated.length > 0 || identityChanged || blockedChanged)
161
+ writeProviderStore(worldHome, effective);
162
+ return { ...result, store: effective };
179
163
  }
180
164
  /** The view for one (already loaded) store. */
181
165
  function viewOf(worldHome, codexHome, store, syncedAt, note) {
@@ -189,13 +173,19 @@ function viewOf(worldHome, codexHome, store, syncedAt, note) {
189
173
  // Only a SEEDED keyed entry defers to auth.json, so only it can be shadowed
190
174
  // by a ChatGPT login. An entry holding its own key bypasses the auth manager
191
175
  // entirely through the launch's synthetic `env_key`.
192
- const deferred = store.providers.some((entry) => entry.auth.kind === "apiKey" && entry.auth.value === "");
176
+ const deferred = store.providers.some((entry) => entry.kind !== "openai-oauth" && entry.auth.kind === "apiKey" && entry.auth.value === "");
193
177
  const shadowed = deferred && chatgptAuthMode(codexHome)
194
178
  ? 'Codex is signed in with a ChatGPT account, which takes precedence over a saved API key — a provider that relies on auth.json will not run until you sign out (`codex logout`) or give this provider its own key.'
195
179
  : undefined;
196
- const combined = [note, shadowed].filter((part) => part !== undefined).join(" ");
180
+ const nativeAlias = readNativeProviders(codexHome).some(entry => entry.id === OPENAI_OAUTH_ID);
181
+ const blocked = store.blockedNativeIds?.includes(OPENAI_OAUTH_ID) === true;
182
+ const duplicate = store.providers.filter(entry => entry.id === OPENAI_OAUTH_ID).length > 1;
183
+ const collision = blocked || duplicate || (nativeAlias && store.providers.some(entry => entry.kind === "openai-oauth"))
184
+ ? 'OpenAI OAuth discovery is blocked by an openai-oauth provider ID conflict. Existing third-party routes and keys are preserved. Rename the conflicting native provider table; historical session routes need explicit repair before this ID can be reused.'
185
+ : undefined;
186
+ const combined = [note, shadowed, collision].filter((part) => part !== undefined).join(" ");
197
187
  return {
198
- providers: store.providers.map((entry) => project(codexHome, entry)),
188
+ providers: store.providers.filter(entry => !(duplicate && entry.id === OPENAI_OAUTH_ID)).map((entry) => project(codexHome, entry)),
199
189
  native,
200
190
  protocols: PROTOCOL_OPTIONS,
201
191
  paths: {
@@ -243,6 +233,8 @@ export function normalizeSaveRequest(request, existing) {
243
233
  }
244
234
  const raw = request["provider"];
245
235
  const id = raw["id"];
236
+ if (id === OPENAI_OAUTH_ID && (existing === undefined || existing.kind === "openai-oauth"))
237
+ throw new TypeError("OpenAI OAuth is a reserved native provider; configure authentication in Codex");
246
238
  if (!isProviderId(id)) {
247
239
  throw new TypeError(`codexProviders/save: id must match /^[a-z0-9][a-z0-9-]*$/ (got ${JSON.stringify(id)})`);
248
240
  }
@@ -328,6 +320,8 @@ export function normalizeSaveRequest(request, existing) {
328
320
  export function saveProvider(worldHome, codexHome, request, syncedAt) {
329
321
  const store = readProviderStore(worldHome);
330
322
  const id = isRecord(request) && isRecord(request["provider"]) ? request["provider"]["id"] : undefined;
323
+ if (store.providers.filter(entry => entry.id === id).length > 1)
324
+ throw new TypeError("codexProviders/save: duplicate provider ID is ambiguous; explicitly repair the conflicting identities before editing");
331
325
  const existing = store.providers.find((entry) => entry.id === id);
332
326
  const entry = normalizeSaveRequest(request, existing);
333
327
  const next = upsertProvider(store, entry);
@@ -340,6 +334,10 @@ export function removeProviderEntry(worldHome, codexHome, request, syncedAt) {
340
334
  if (id === undefined)
341
335
  throw new TypeError("codexProviders/remove: id must be a non-empty string");
342
336
  const store = readProviderStore(worldHome);
337
+ if (id === OPENAI_OAUTH_ID && store.providers.some(entry => entry.id === id && entry.kind === "openai-oauth"))
338
+ throw new TypeError("OpenAI OAuth is a read-only native provider");
339
+ if (store.providers.filter(entry => entry.id === id).length > 1)
340
+ throw new TypeError("codexProviders/remove: duplicate provider ID is ambiguous; explicitly repair the conflicting identities before removing");
343
341
  const next = removeProvider(store, id);
344
342
  writeProviderStore(worldHome, next);
345
343
  return viewOf(worldHome, codexHome, next, syncedAt);
@@ -99,6 +99,9 @@ export function providerEntry(value) {
99
99
  if (value === null || typeof value !== "object")
100
100
  return undefined;
101
101
  const record = value;
102
+ if (record["kind"] === "openai-oauth" && record["id"] === "openai-oauth") {
103
+ return { id: "openai-oauth", label: "OpenAI OAuth", kind: "openai-oauth", origin: "native", baseUrl: "", protocol: "responses", auth: { kind: "apiKey", value: "" }, models: [] };
104
+ }
102
105
  const id = record["id"];
103
106
  const label = str(record["label"]);
104
107
  const baseUrl = str(record["baseUrl"]);
@@ -144,7 +147,9 @@ export function readProviderStore(worldHome) {
144
147
  const rawRemoved = record["removed"];
145
148
  const removed = (Array.isArray(rawRemoved) ? rawRemoved : [])
146
149
  .filter((value) => typeof value === "string" && value.trim() !== "");
147
- return { version: 1, providers, ...(removed.length === 0 ? {} : { removed }) };
150
+ const blockedNativeIds = Array.isArray(record["blockedNativeIds"])
151
+ ? [...new Set(record["blockedNativeIds"].filter((id) => id === "openai-oauth"))] : [];
152
+ return { version: 1, providers, ...(removed.length === 0 ? {} : { removed }), ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
148
153
  }
149
154
  /**
150
155
  * Persist the store atomically (tmp + rename) and 0600 — the file holds API
@@ -155,7 +160,7 @@ export function writeProviderStore(worldHome, store) {
155
160
  const path = providerStorePath(worldHome);
156
161
  mkdirSync(dirname(path), { recursive: true });
157
162
  const temp = `${path}.tmp-${process.pid}`;
158
- writeFileSync(temp, `${JSON.stringify(store, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
163
+ writeFileSync(temp, `${JSON.stringify({ ...store, providers: store.providers.map(entry => entry.kind === "openai-oauth" ? { id: entry.id, label: entry.label, kind: entry.kind, origin: "native" } : entry) }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
159
164
  renameSync(temp, path);
160
165
  }
161
166
  /**
@@ -176,7 +181,8 @@ export function upsertProvider(store, entry) {
176
181
  // carry the tombstone forward, and the next sync would keep skipping the
177
182
  // endpoint the user just re-added.
178
183
  const { removed: _stale, ...rest } = store;
179
- return { ...rest, providers, ...(removed.length === 0 ? {} : { removed }) };
184
+ const blockedNativeIds = [...new Set([...(store.blockedNativeIds ?? []), ...(entry.id === "openai-oauth" && entry.kind !== "openai-oauth" ? [entry.id] : [])])];
185
+ return { ...rest, providers, ...(removed.length === 0 ? {} : { removed }), ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
180
186
  }
181
187
  /**
182
188
  * Drop one provider, and TOMBSTONE its endpoint so the reconcile cannot bring
@@ -193,7 +199,8 @@ export function removeProvider(store, id) {
193
199
  return { ...store, providers };
194
200
  const key = endpointKey(entry.baseUrl);
195
201
  const removed = store.removed?.includes(key) === true ? store.removed : [...store.removed ?? [], key];
196
- return { ...store, providers, removed };
202
+ const blockedNativeIds = [...new Set([...(store.blockedNativeIds ?? []), ...(entry.id === "openai-oauth" && entry.kind !== "openai-oauth" ? [entry.id] : [])])];
203
+ return { ...store, providers, removed, ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
197
204
  }
198
205
  /**
199
206
  * The comparison key for "the same endpoint". Codex itself treats two provider
@@ -330,7 +337,13 @@ export function syncNativeProviders(store, imported) {
330
337
  if (store.removed?.includes(key) === true)
331
338
  continue;
332
339
  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);
340
+ // A native identity is not an endpoint table. Never refresh it, rename a
341
+ // conflicting native table behind the user's back, or select a duplicate id.
342
+ if (wantedId === "openai-oauth" && (providers.some(entry => entry.id === wantedId && entry.kind === "openai-oauth")
343
+ || providers.filter(entry => entry.id === wantedId).length > 1))
344
+ continue;
345
+ const at = providers.findIndex((entry) => entry.kind !== "openai-oauth" &&
346
+ ((wantedId !== undefined && entry.id === wantedId) || endpointKey(entry.baseUrl) === key));
334
347
  if (at === -1) {
335
348
  const id = uniqueProviderId(wantedId ?? providerIdFromEndpoint(baseUrl), providers.map((entry) => entry.id));
336
349
  providers.push({
@@ -370,7 +383,7 @@ export function syncNativeProviders(store, imported) {
370
383
  }
371
384
  const removed = store.removed ?? [];
372
385
  const next = removed.length === 0
373
- ? { version: 1, providers }
374
- : { version: 1, providers, removed };
386
+ ? { ...store, version: 1, providers }
387
+ : { ...store, version: 1, providers, removed };
375
388
  return { store: next, added, updated };
376
389
  }
@@ -1,41 +1,4 @@
1
- /**
2
- * codex-sdk-launch.ts — how a provider reaches the in-process Codex SDK
3
- * (2026-10-04 multi-provider rework).
4
- *
5
- * This is the SDK half of `agent-codex/codex-launch.ts`. That module turns a
6
- * provider into argv for a `codex app-server` child; this one turns the same
7
- * provider into the structured object the SDK's `CodexOptions.config` takes —
8
- * and underneath they are the same mechanism. The SDK's own type docs say it
9
- * "flatten[s] it into dotted paths and serialize[s] values as TOML literals so
10
- * they are compatible with the CLI's `--config` parsing", which is exactly the
11
- * `--config key=value` channel the app-server line drives by hand. A provider
12
- * therefore costs nothing on disk on this path either, and is scoped to the
13
- * process that builds the config, which is what a per-session provider choice
14
- * needs.
15
- *
16
- * The one transport difference is env. A spawned app-server child inherits the
17
- * process environment, so `codex-launch.ts` hands back ADDITIONS and its caller
18
- * spreads them (`env: { ...process.env, ...launch.env }`). The SDK's `env` is
19
- * REPLACE semantics — "When provided, the SDK will not inherit variables from
20
- * `process.env`" — so the caller must spread the same way before constructing
21
- * `Codex`. This module still hands back additions only: composing the full map
22
- * belongs to the caller, and keeping the addition set small is what makes the
23
- * synthetic-key branch legible in tests.
24
- *
25
- * The measured constraints `codex-launch.ts` encodes carry over unchanged (see
26
- * `docs/test-reports/2026-10-03-codex-provider-model-switch-probe.md`):
27
- *
28
- * - a HYPHENATED id is fine in a dotted path (`model_providers.zhipu-cn.…`),
29
- * so native table names survive verbatim as provider ids;
30
- * - `model_catalog_json` is resolved against the process CWD when it arrives
31
- * as an override, so the path is always absolute;
32
- * - `env_key` is the one auth shape that never consults the auth manager, so a
33
- * stored API key rides the child's environment under a synthetic variable
34
- * name. That is also what makes a keyed provider usable behind a ChatGPT
35
- * login, where `requires_openai_auth` is silently shadowed.
36
- *
37
- * @module agent-codex-sdk/codex-sdk-launch
38
- */
1
+ import { officialCatalogForLaunch } from "./codex-oauth.js";
39
2
  import { writeProviderCatalog } from "./codex-native-config.js";
40
3
  /** The environment variable prefix for a key this adapter injects itself. */
41
4
  const SYNTHETIC_KEY_PREFIX = "SUPERD_CODEX_KEY_";
@@ -54,6 +17,10 @@ export function syntheticKeyName(providerId) {
54
17
  * @returns the structured config, the environment additions, and the provider id they pin.
55
18
  */
56
19
  export function providerSdkLaunch(worldHome, codexHome, provider) {
20
+ if (provider.kind === "openai-oauth") {
21
+ const catalog = officialCatalogForLaunch(codexHome);
22
+ return { config: { model_provider: "openai", openai_base_url: "", ...(catalog === undefined ? {} : { model_catalog_json: catalog }) }, env: {}, providerId: "openai" };
23
+ }
57
24
  // The SDK serializes these values as TOML literals itself, so unlike the argv
58
25
  // line there is no hand-quoting here: a bare object is the whole override.
59
26
  const table = {
@@ -1,3 +1,4 @@
1
+ import { OPENAI_OAUTH_ID, readEffectiveProviderStore } from "./codex-oauth.js";
1
2
  /**
2
3
  * agent-codex provider — Codex provider plugin for DeepSeek Harness
3
4
  * (Super D, SDK line).
@@ -30,7 +31,6 @@ import { CodexLlmAdapter, routeIds } from "./adapter.js";
30
31
  import { registerCodexPreset } from "./agent-preset-codex.js";
31
32
  import { readRolloutHead, resolveCodexHome } from "./codex-store.js";
32
33
  import { CodexResumeError, sessionRecord, upsertSession } from "./session-map.js";
33
- import { readProviderStore } from "./codex-providers.js";
34
34
  import { readNativeSelection } from "./codex-native-config.js";
35
35
  import { CodexProvidersGateway, syncFromNative } from "./codex-provider-gateway.js";
36
36
  import { seedRoute } from "./codex-route.js";
@@ -157,12 +157,12 @@ export class CodexProvider extends Service {
157
157
  */
158
158
  syncNow() {
159
159
  try {
160
- const { store, added, updated } = syncFromNative(this.home, resolveCodexHome());
160
+ const { added, updated } = syncFromNative(this.home, resolveCodexHome());
161
161
  if (added.length > 0 || updated.length > 0) {
162
162
  this.runtime.ctx.logger.info(`codex-provider: config.toml sync — added ${added.join(", ") || "none"}, refreshed ${updated.join(", ") || "none"}`);
163
163
  }
164
164
  this.syncedAt = Date.now();
165
- this.#applyRoutes(store.providers);
165
+ this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
166
166
  }
167
167
  catch (error) {
168
168
  this.runtime.ctx.logger.warn(`codex-provider: provider sync failed: ${String(error)}`);
@@ -171,7 +171,7 @@ export class CodexProvider extends Service {
171
171
  /** Re-register the `ctx.llm` routes from the current store (panel mutation path). */
172
172
  refreshRoutes() {
173
173
  try {
174
- this.#applyRoutes(readProviderStore(this.home).providers);
174
+ this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
175
175
  }
176
176
  catch (error) {
177
177
  this.runtime.ctx.logger.warn(`codex-provider: route refresh failed: ${String(error)}`);
@@ -186,14 +186,14 @@ export class CodexProvider extends Service {
186
186
  nativeProviderId() {
187
187
  const native = readNativeSelection(resolveCodexHome());
188
188
  const wanted = native.providerId;
189
- if (wanted === undefined)
190
- return undefined;
191
- const providers = readProviderStore(this.home).providers;
192
- return providers.find((entry) => entry.id === wanted)?.id ?? undefined;
189
+ if (wanted === undefined || wanted === "openai")
190
+ return readEffectiveProviderStore(this.home, resolveCodexHome()).providers.some(entry => entry.kind === "openai-oauth") ? OPENAI_OAUTH_ID : undefined;
191
+ const providers = readEffectiveProviderStore(this.home, resolveCodexHome()).providers;
192
+ return providers.find((entry) => entry.kind !== "openai-oauth" && entry.id === wanted)?.id ?? undefined;
193
193
  }
194
194
  /** The store entry a DSH route id names. */
195
195
  providerEntry(providerId) {
196
- return readProviderStore(this.home).providers.find((entry) => entry.id === providerId);
196
+ return readEffectiveProviderStore(this.home, resolveCodexHome()).providers.find((entry) => entry.id === providerId);
197
197
  }
198
198
  /**
199
199
  * The factory's session-level route fallback — always ONE coherent pair.
@@ -215,8 +215,13 @@ export class CodexProvider extends Service {
215
215
  sessionAgentOptions(requested) {
216
216
  const native = readNativeSelection(resolveCodexHome());
217
217
  const wanted = requested?.provider !== undefined && typeof requested.model === "string" ? requested : undefined;
218
+ const requestedEntry = wanted === undefined ? undefined : this.providerEntry(wanted.provider);
219
+ // An explicitly selected native identity must survive unavailable auth or
220
+ // metadata. Its launch fails honestly instead of seeding a third-party route.
221
+ if (wanted?.provider === OPENAI_OAUTH_ID && (requestedEntry === undefined || requestedEntry.kind === "openai-oauth"))
222
+ return { ...wanted };
218
223
  const serves = wanted !== undefined
219
- && this.providerEntry(wanted.provider)?.models.some((model) => model.id === wanted.model) === true;
224
+ && requestedEntry?.models.some((model) => model.id === wanted.model) === true;
220
225
  if (wanted !== undefined && serves)
221
226
  return { ...wanted, provider: wanted.provider, model: wanted.model };
222
227
  const provider = this.nativeProviderId();
@@ -272,6 +277,8 @@ export class CodexProvider extends Service {
272
277
  // the user's home (the per-provider model catalog is ours).
273
278
  launchFor: (providerId) => {
274
279
  const entry = this.providerEntry(providerId);
280
+ if (entry === undefined && providerId === OPENAI_OAUTH_ID)
281
+ throw new Error("OpenAI OAuth authentication is unavailable; sign in using Codex");
275
282
  return entry === undefined ? undefined : providerSdkLaunch(this.home, codexHome, entry);
276
283
  },
277
284
  contextWindowFor: (providerId, model) => {
@@ -374,7 +381,7 @@ export class CodexProvider extends Service {
374
381
  * picker would be noise.
375
382
  */
376
383
  #registerModelCatalog() {
377
- this.#applyRoutes(readProviderStore(this.home).providers);
384
+ this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
378
385
  }
379
386
  /** Register or atomically re-point the codex routes at the given providers. */
380
387
  #applyRoutes(providers) {
@@ -52,8 +52,8 @@ export const SDK_PACKAGE = "@openai/codex-sdk";
52
52
  /** The native binary package the SDK resolves at run time; pinned with the SDK. */
53
53
  export const SDK_BINARY_PACKAGE = "@openai/codex";
54
54
  /**
55
- * Pin of BOTH packages — the same 0.154.0 the app-server (primary) line pins,
56
- * so the two Codex faces agree on the wire behaviour they bridge.
55
+ * Pin of BOTH SDK runtime packages; the optional SDK line keeps its tested 0.154.0 pair,
56
+ * while the primary app-server line independently discovers installed native cores.
57
57
  */
58
58
  export const SDK_PACKAGE_VERSION = "0.154.0";
59
59
  /** The entry `loadCodexSdk` imports, relative to the runtime dir. */
@@ -291,6 +291,12 @@ export class HermesAgent {
291
291
  */
292
292
  #remoteQueue = [];
293
293
  #bridgedToolResults = new Set();
294
+ /** callIds already advertised by an appended assistant/message frame — the
295
+ * strict validator refuses any `tool/call` whose callId no assistant frame
296
+ * advertised (2026-10-04 dev3 case C: the hermes wire never delivers the
297
+ * tool_use-bearing intermediate assistant messages, so execution frames
298
+ * arrived with no advertisement anchor). */
299
+ #advertisedToolCalls = new Set();
294
300
  /** Fires when the agent has been idle past HERMES_IDLE_EXIT_MS (tears down the client). */
295
301
  #onIdleExit;
296
302
  #idleExitTimer = undefined;
@@ -495,7 +501,8 @@ export class HermesAgent {
495
501
  return;
496
502
  this.session.append("turn/start", { turn: this.#dashTurn });
497
503
  await this.#emitRequestContext();
498
- this.#titleSourceSeq ??= this.session.append("user/message", message, { surfaceOp: "append" }).seq;
504
+ const event = this.session.append("user/message", message, { surfaceOp: "append" });
505
+ this.#titleSourceSeq ??= event.seq;
499
506
  this.#armTurnDeadline();
500
507
  void this.#client
501
508
  .prompt(userMessageText(message))
@@ -525,6 +532,12 @@ export class HermesAgent {
525
532
  await this.#handshake(`session.create`, () => this.#client.createSession({
526
533
  cwd: sessionCtx?.cwd ?? process.cwd(),
527
534
  ...(route?.model !== undefined ? { model: route.model } : {}),
535
+ // The route's provider slug MUST ride with the model: without it the
536
+ // gateway pins the session to its default provider (deepseek) and any
537
+ // foreign-family model id (z-ai/glm-*, anthropic/*, …) dies at
538
+ // session.create with `Model … is not served by provider 'deepseek'`
539
+ // (-32602). The gateway normalizes aliases (`z-ai` → `zai`) itself.
540
+ ...(route?.provider !== undefined ? { provider: route.provider } : {}),
528
541
  ...(sessionCtx?.title !== undefined && sessionCtx.title !== "" ? { title: sessionCtx.title } : {}),
529
542
  }));
530
543
  }
@@ -892,13 +905,40 @@ export class HermesAgent {
892
905
  break;
893
906
  const callId = String(event.toolCallId ?? "");
894
907
  const name = String(event.toolName ?? "");
908
+ const argumentsJson = String(event.argumentsJson ?? JSON.stringify(event.args ?? {}));
895
909
  this.#commitStepStart();
910
+ // Advertisement-before-execution (validator invariant): the hermes wire
911
+ // never delivers the tool_use-bearing intermediate assistant messages,
912
+ // so unless a real assistant frame already advertised this callId,
913
+ // bridge a minimal assistant/message carrying the tool-call block —
914
+ // arguments copied verbatim into both frames (validator: lifecycle
915
+ // arguments ≡ advertised, strict string equality; 2026-10-04 dev3
916
+ // case C). A live stream bridge is left untouched — the final real
917
+ // assistant message settles it.
918
+ if (!this.#advertisedToolCalls.has(callId)) {
919
+ const advertisement = createAssistantMessage({
920
+ content: [{ type: "tool-call", id: ToolCallId(callId), name, arguments: argumentsJson }],
921
+ source: {
922
+ provider: String(this.options.provider ?? ""),
923
+ model: String(this.options.model ?? ""),
924
+ },
925
+ });
926
+ this.session.append("assistant/message", {
927
+ turn: this.#dashTurn,
928
+ step: this.#step,
929
+ message: advertisement,
930
+ stream: [],
931
+ }, {
932
+ surfaceOp: "append",
933
+ });
934
+ this.#advertisedToolCalls.add(callId);
935
+ }
896
936
  this.session.append("tool/call", {
897
937
  turn: this.#dashTurn,
898
938
  step: this.#step,
899
939
  callId: ToolCallId(callId),
900
940
  name,
901
- arguments: String(event.argumentsJson ?? JSON.stringify(event.args ?? {})),
941
+ arguments: argumentsJson,
902
942
  });
903
943
  break;
904
944
  }
@@ -1016,6 +1056,10 @@ export class HermesAgent {
1016
1056
  }
1017
1057
  #appendAssistantMessage(message, usage) {
1018
1058
  const content = convertContent(message.content);
1059
+ for (const block of content) {
1060
+ if (block.type === "tool-call")
1061
+ this.#advertisedToolCalls.add(block.id);
1062
+ }
1019
1063
  const assistant = createAssistantMessage({
1020
1064
  content,
1021
1065
  source: {