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
@@ -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
  }
@@ -0,0 +1,120 @@
1
+ /** Resolve an installed Codex runtime at first use, without modifying its home. */
2
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import { posix, win32 } from "node:path";
6
+ function readableFile(path) {
7
+ try {
8
+ accessSync(path, constants.R_OK);
9
+ return statSync(path).isFile();
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ function executable(path) {
16
+ try {
17
+ accessSync(path, constants.X_OK);
18
+ return statSync(path).isFile();
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function desktopPaths(discovery) {
25
+ if (discovery.desktopCandidates !== undefined)
26
+ return discovery.desktopCandidates;
27
+ const explicit = discovery.env.CODEX_DESKTOP_BIN?.trim();
28
+ const paths = explicit ? [explicit] : [];
29
+ if (discovery.platform === "linux") {
30
+ paths.push("/usr/lib/chatgpt/resources/codex", "/usr/lib/codex/resources/codex", "/opt/Codex/resources/codex", "/opt/codex/resources/codex");
31
+ }
32
+ else if (discovery.platform === "darwin") {
33
+ for (const root of ["/Applications", posix.join(discovery.homeDir, "Applications")]) {
34
+ for (const app of ["Codex.app", "ChatGPT.app"])
35
+ paths.push(posix.join(root, app, "Contents/Resources/codex"));
36
+ }
37
+ }
38
+ else if (discovery.platform === "win32") {
39
+ const local = discovery.env.LOCALAPPDATA;
40
+ if (local) {
41
+ for (const app of ["Codex", "ChatGPT"])
42
+ paths.push(win32.join(local, "Programs", app, "resources", "codex.exe"));
43
+ }
44
+ }
45
+ return paths;
46
+ }
47
+ function isNodeScript(command, discovery) {
48
+ let target = command;
49
+ try {
50
+ target = discovery.realpath(command);
51
+ }
52
+ catch {
53
+ // Explicit nonexistent paths are rejected by the file check below.
54
+ }
55
+ return /\.(?:[cm]?js)$/i.test(target);
56
+ }
57
+ export function resolveCodexRuntime(options = {}) {
58
+ const platform = options.platform ?? process.platform;
59
+ const env = options.env ?? process.env;
60
+ const isFile = options.isFile ?? readableFile;
61
+ const isExecutable = options.isExecutable ?? executable;
62
+ const path = platform === "win32" ? win32 : posix;
63
+ const shellWrapper = (command) => platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
64
+ const discovery = {
65
+ platform,
66
+ env,
67
+ homeDir: options.homeDir ?? homedir(),
68
+ ...(options.desktopCandidates === undefined ? {} : { desktopCandidates: options.desktopCandidates }),
69
+ isFile,
70
+ isExecutable,
71
+ realpath: options.realpath ?? realpathSync,
72
+ findCommand: options.findCommand ?? ((name) => {
73
+ const suffixes = platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
74
+ for (const directory of (env.PATH ?? "").split(platform === "win32" ? ";" : ":")) {
75
+ if (directory === "")
76
+ continue;
77
+ for (const suffix of suffixes) {
78
+ const candidate = path.join(directory, `${name}${suffix}`);
79
+ if (shellWrapper(candidate))
80
+ continue;
81
+ if (isFile(candidate) && isExecutable(candidate))
82
+ return candidate;
83
+ }
84
+ }
85
+ return undefined;
86
+ }),
87
+ resolvePackageEntry: options.resolvePackageEntry ?? (() => createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js")),
88
+ };
89
+ const usable = (command) => !shellWrapper(command) && isFile(command) && (isNodeScript(command, discovery) || isExecutable(command));
90
+ const selected = (command, source) => ({ command, nodeScript: isNodeScript(command, discovery), source });
91
+ const override = env.CODEX_BIN?.trim();
92
+ if (override) {
93
+ const command = /[\\/]/.test(override) ? override : discovery.findCommand(override) ?? override;
94
+ if (shellWrapper(command))
95
+ throw new Error(`CODEX_BIN ${override} is a Windows .cmd/.bat shell wrapper. Set CODEX_BIN to the native codex.exe executable or the @openai/codex JavaScript entry instead.`);
96
+ if (!usable(command))
97
+ throw new Error(`CODEX_BIN ${override} is not a readable JavaScript entry or executable Codex runtime. Fix CODEX_BIN or remove the override.`);
98
+ return selected(command, "override");
99
+ }
100
+ for (const candidate of desktopPaths(discovery)) {
101
+ if (!shellWrapper(candidate) && isFile(candidate) && isExecutable(candidate))
102
+ return selected(candidate, "desktop");
103
+ }
104
+ const cli = discovery.findCommand("codex");
105
+ if (cli !== undefined && usable(cli))
106
+ return selected(cli, "cli");
107
+ try {
108
+ const entry = discovery.resolvePackageEntry();
109
+ if (isFile(entry))
110
+ return { command: entry, nodeScript: true, source: "package" };
111
+ }
112
+ catch {
113
+ // Installation fallback only; authentication/generation never reaches here.
114
+ }
115
+ throw new Error("cannot locate a Codex runtime: install Codex desktop or a user CLI on PATH, restore the exact-pin @openai/codex package, or set CODEX_BIN to its executable or JavaScript entry.");
116
+ }
117
+ /** Compatibility interface for callers that need only the selected path. */
118
+ export function resolveCodexCommand(options = {}) {
119
+ return resolveCodexRuntime(options).command;
120
+ }
@@ -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, app-server line).
@@ -29,7 +30,6 @@ import { CodexLlmAdapter, routeIds } from "./adapter.js";
29
30
  import { registerCodexPreset } from "./agent-preset-codex.js";
30
31
  import { readRolloutHead, resolveCodexHome } from "./codex-store.js";
31
32
  import { CodexProvidersGateway, syncFromNative } from "./codex-provider-gateway.js";
32
- import { readProviderStore } from "./codex-providers.js";
33
33
  import { readNativeSelection } from "./codex-native-config.js";
34
34
  import { seedRoute } from "./codex-route.js";
35
35
  import { providerLaunch } from "./codex-launch.js";
@@ -159,12 +159,12 @@ export class CodexProvider extends Service {
159
159
  */
160
160
  syncNow() {
161
161
  try {
162
- const { store, added, updated } = syncFromNative(this.home, resolveCodexHome());
162
+ const { added, updated } = syncFromNative(this.home, resolveCodexHome());
163
163
  if (added.length > 0 || updated.length > 0) {
164
164
  this.runtime.ctx.logger.info(`codex-provider: config.toml sync — added ${added.join(", ") || "none"}, refreshed ${updated.join(", ") || "none"}`);
165
165
  }
166
166
  this.syncedAt = Date.now();
167
- this.#applyRoutes(store.providers);
167
+ this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
168
168
  }
169
169
  catch (error) {
170
170
  this.runtime.ctx.logger.warn(`codex-provider: provider sync failed: ${String(error)}`);
@@ -173,7 +173,7 @@ export class CodexProvider extends Service {
173
173
  /** Re-register the `ctx.llm` routes from the current store (panel mutation path). */
174
174
  refreshRoutes() {
175
175
  try {
176
- this.#applyRoutes(readProviderStore(this.home).providers);
176
+ this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
177
177
  }
178
178
  catch (error) {
179
179
  this.runtime.ctx.logger.warn(`codex-provider: route refresh failed: ${String(error)}`);
@@ -188,14 +188,14 @@ export class CodexProvider extends Service {
188
188
  nativeProviderId() {
189
189
  const native = readNativeSelection(resolveCodexHome());
190
190
  const wanted = native.providerId;
191
- if (wanted === undefined)
192
- return undefined;
193
- const providers = readProviderStore(this.home).providers;
194
- return providers.find((entry) => entry.id === wanted)?.id ?? undefined;
191
+ if (wanted === undefined || wanted === "openai")
192
+ return readEffectiveProviderStore(this.home, resolveCodexHome()).providers.some(entry => entry.kind === "openai-oauth") ? OPENAI_OAUTH_ID : undefined;
193
+ const providers = readEffectiveProviderStore(this.home, resolveCodexHome()).providers;
194
+ return providers.find((entry) => entry.kind !== "openai-oauth" && entry.id === wanted)?.id ?? undefined;
195
195
  }
196
196
  /** The store entry a DSH route id names. */
197
197
  providerEntry(providerId) {
198
- return readProviderStore(this.home).providers.find((entry) => entry.id === providerId);
198
+ return readEffectiveProviderStore(this.home, resolveCodexHome()).providers.find((entry) => entry.id === providerId);
199
199
  }
200
200
  /**
201
201
  * The factory's session-level route fallback — always ONE coherent pair.
@@ -217,8 +217,13 @@ export class CodexProvider extends Service {
217
217
  sessionAgentOptions(requested) {
218
218
  const native = readNativeSelection(resolveCodexHome());
219
219
  const wanted = requested?.provider !== undefined && typeof requested.model === "string" ? requested : undefined;
220
+ const requestedEntry = wanted === undefined ? undefined : this.providerEntry(wanted.provider);
221
+ // An explicitly selected native identity must survive unavailable auth or
222
+ // metadata. Its launch fails honestly instead of seeding a third-party route.
223
+ if (wanted?.provider === OPENAI_OAUTH_ID && (requestedEntry === undefined || requestedEntry.kind === "openai-oauth"))
224
+ return { ...wanted };
220
225
  const serves = wanted !== undefined
221
- && this.providerEntry(wanted.provider)?.models.some((model) => model.id === wanted.model) === true;
226
+ && requestedEntry?.models.some((model) => model.id === wanted.model) === true;
222
227
  if (wanted !== undefined && serves)
223
228
  return { ...wanted, provider: wanted.provider, model: wanted.model };
224
229
  const provider = this.nativeProviderId();
@@ -274,6 +279,8 @@ export class CodexProvider extends Service {
274
279
  // the user's home (the per-provider model catalog is ours).
275
280
  launchFor: (providerId) => {
276
281
  const entry = this.providerEntry(providerId);
282
+ if (entry === undefined && providerId === OPENAI_OAUTH_ID)
283
+ throw new Error("OpenAI OAuth authentication is unavailable; sign in using Codex");
277
284
  return entry === undefined ? undefined : providerLaunch(this.home, codexHome, entry);
278
285
  },
279
286
  contextWindowFor: (providerId, model) => {
@@ -379,7 +386,7 @@ export class CodexProvider extends Service {
379
386
  * picker would be noise.
380
387
  */
381
388
  #registerModelCatalog() {
382
- this.#applyRoutes(readProviderStore(this.home).providers);
389
+ this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
383
390
  }
384
391
  /** Register or atomically re-point the codex routes at the given providers. */
385
392
  #applyRoutes(providers) {
@@ -22,7 +22,7 @@
22
22
  "test": "node --test test/*.test.mjs"
23
23
  },
24
24
  "dependencies": {
25
- "@openai/codex": "0.154.0",
25
+ "@openai/codex": "0.160.0",
26
26
  "zod": "4.4.3"
27
27
  },
28
28
  "peerDependencies": {
@@ -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;
@@ -276,6 +276,11 @@ export class CodexAgent {
276
276
  */
277
277
  #remoteQueue = [];
278
278
  #bridgedToolResults = new Set();
279
+ /** callIds already advertised by an appended assistant/message frame — the
280
+ * strict validator refuses any `tool/call` whose callId no assistant frame
281
+ * advertised, and requires the execution arguments to equal the advertised
282
+ * string (2026-10-04 dev3 omp/hermes repair: same write-side family). */
283
+ #advertisedToolCalls = new Set();
279
284
  /**
280
285
  * The open turn's FINAL assistant message, buffered from `message_end`
281
286
  * until `turn_end` so the SAME turn's usage can land on it (the DSH
@@ -894,7 +899,32 @@ export class CodexAgent {
894
899
  case "tool_execution_start": {
895
900
  const callId = String(event.toolCallId ?? "");
896
901
  const name = String(event.toolName ?? "");
902
+ const argumentsJson = String(event.argumentsJson ?? JSON.stringify(event.args ?? {}));
897
903
  this.#commitStepStart();
904
+ // Advertisement-before-execution (validator invariant, 2026-10-04 dev3
905
+ // omp/hermes repair family): unless a real assistant frame advertised
906
+ // this callId, bridge a minimal assistant/message carrying the
907
+ // tool-call block — arguments copied verbatim into both frames. The
908
+ // synthesized source mirrors #appendAssistantMessage's open-turn
909
+ // fallback chain.
910
+ if (!this.#advertisedToolCalls.has(callId)) {
911
+ const advertisement = createAssistantMessage({
912
+ content: [{ type: "tool-call", id: ToolCallId(callId), name, arguments: argumentsJson }],
913
+ source: {
914
+ provider: String(this.#turnRoute?.provider ?? this.options.provider ?? ""),
915
+ model: String(this.#turnRoute?.model ?? this.options.model ?? ""),
916
+ },
917
+ });
918
+ this.session.append("assistant/message", {
919
+ turn: this.#dashTurn,
920
+ step: this.#step,
921
+ message: advertisement,
922
+ stream: [],
923
+ }, {
924
+ surfaceOp: "append",
925
+ });
926
+ this.#advertisedToolCalls.add(callId);
927
+ }
898
928
  this.session.append("tool/call", {
899
929
  turn: this.#dashTurn,
900
930
  step: this.#step,
@@ -903,7 +933,7 @@ export class CodexAgent {
903
933
  // The DSH payload requires the RAW JSON string the model produced;
904
934
  // the wire carries it verbatim for MCP calls and renders it from the
905
935
  // discrete fields for the other item shapes.
906
- arguments: String(event.argumentsJson ?? JSON.stringify(event.args ?? {})),
936
+ arguments: argumentsJson,
907
937
  });
908
938
  break;
909
939
  }
@@ -1015,6 +1045,10 @@ export class CodexAgent {
1015
1045
  }
1016
1046
  #appendAssistantMessage(message, usage) {
1017
1047
  const content = convertContent(message.content);
1048
+ for (const block of content) {
1049
+ if (block.type === "tool-call")
1050
+ this.#advertisedToolCalls.add(block.id);
1051
+ }
1018
1052
  const assistant = createAssistantMessage({
1019
1053
  content,
1020
1054
  source: {
@@ -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.