@yansigit/opencodex 2.36.1-dev.20260829.42 → 2.36.1-dev.20260829.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{ApiKeys-C_cz6Rww.js → ApiKeys-LOOFiZfv.js} +1 -1
- package/gui/dist/assets/{Claude-ofGguLfF.js → Claude-BwKzpXe3.js} +1 -1
- package/gui/dist/assets/{CodexSet-fhGiVwxe.js → CodexSet-B5qi9KeE.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B-dI69Kn.js → FileIntegrationPage-B0sUef6W.js} +1 -1
- package/gui/dist/assets/{Grok-CgkBu_Sm.js → Grok-bsb4n-f4.js} +1 -1
- package/gui/dist/assets/{Integrations-CTWMNzkD.js → Integrations-CSMwFDvM.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-BKZ0dZnj.js → IntegrationsOverview-CTORdKJA.js} +1 -1
- package/gui/dist/assets/{Logs-6r90C0x4.js → Logs-BWYbfELf.js} +1 -1
- package/gui/dist/assets/{Models-CpcS1zZr.js → Models-CexEtdT1.js} +1 -1
- package/gui/dist/assets/{NumberStepper-C70ttiJY.js → NumberStepper-BapSFQnW.js} +1 -1
- package/gui/dist/assets/{Providers-BROTzuSF.js → Providers-BwUFeAgA.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-DR5vgFXB.js → RestoreDialog-CD8piq90.js} +1 -1
- package/gui/dist/assets/{Startup-BSoZDYbS.js → Startup-BgX731C2.js} +1 -1
- package/gui/dist/assets/{Storage-CUvBwbeB.js → Storage-Bgq7HphP.js} +1 -1
- package/gui/dist/assets/{Subagents-CYu3_DBt.js → Subagents-ChxItbeF.js} +1 -1
- package/gui/dist/assets/{Usage-BV36sd7V.js → Usage-BkylzP50.js} +1 -1
- package/gui/dist/assets/{data-surface-DhcMe0J_.js → data-surface-pCXChiBf.js} +1 -1
- package/gui/dist/assets/{index-DnuNKc0D.js → index-CNopOid3.js} +2 -2
- package/gui/dist/assets/{model-display-BPrQ7F3P.js → model-display-CiUpg8T9.js} +1 -1
- package/gui/dist/assets/{provider-payload-CMb37zWF.js → provider-payload-b2jlS-On.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli/agent.ts +19 -2
- package/src/cli/config-command.ts +7 -3
- package/src/cli/init.ts +2 -2
- package/src/cli/provider.ts +58 -74
- package/src/cli/registry.ts +1 -1
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence.ts +110 -15
- package/src/codex/inject.ts +2 -0
- package/src/codex/subagent-defaults.ts +1 -8
- package/src/codex/subagent-model-authority.ts +189 -0
- package/src/config.ts +209 -11
- package/src/generated/compatibility-version.json +35 -27
- package/src/oauth/index.ts +143 -82
- package/src/oauth/login-cli.ts +53 -11
- package/src/providers/alibaba-region-startup.ts +31 -11
- package/src/providers/api-keys.ts +58 -34
- package/src/providers/key-failover.ts +59 -46
- package/src/providers/model-rename-startup.ts +25 -7
- package/src/providers/openai-tier-startup.ts +38 -14
- package/src/server/index.ts +10 -0
- package/src/server/management/agent-settings-routes.ts +12 -0
- package/src/server/management/context.ts +10 -3
- package/src/server/management/logs-usage-routes.ts +19 -16
- package/src/server/management/model-routes.ts +284 -165
- package/src/server/management/oauth-account-routes.ts +93 -15
- package/src/server/management/provider-routes.ts +200 -87
- package/src/server/responses/collaboration.ts +1 -1
- package/src/storage/policy-input.ts +166 -0
- package/src/storage/policy.ts +59 -211
package/src/cli/provider.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* show <name> Show provider config details (secrets masked)
|
|
9
9
|
* set-default <name> Change the default provider
|
|
10
10
|
*/
|
|
11
|
-
import { hasOwnProvider, isValidProviderName, loadConfig,
|
|
11
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, mutatePersistedConfig, sanitizeModelCostsForDisplay } from "../config";
|
|
12
12
|
import { apiKeyTransportConfigError } from "../config/provider-validation";
|
|
13
13
|
import { hasHelpFlag } from "./help";
|
|
14
14
|
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
|
|
@@ -57,19 +57,26 @@ function maskSecret(value: string): string {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
|
-
// Validation helper
|
|
60
|
+
// Validation helper
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
|
|
63
|
-
function
|
|
63
|
+
function validateProviderConfig(config: ReturnType<typeof loadConfig>): void {
|
|
64
64
|
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
65
|
-
|
|
66
|
-
process.exit(1);
|
|
65
|
+
throw new Error("config would have no providers");
|
|
67
66
|
}
|
|
68
67
|
if (!hasOwnProvider(config.providers, config.defaultProvider)) {
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
throw new Error(`defaultProvider "${config.defaultProvider}" does not exist in providers`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mutateProviderConfig<T>(mutate: (config: ReturnType<typeof loadConfig>) => { changed: boolean; value: T }): T {
|
|
73
|
+
const outcome = mutatePersistedConfig(mutate);
|
|
74
|
+
if (outcome.status === "unavailable") {
|
|
75
|
+
throw new Error(outcome.reason === "conflict"
|
|
76
|
+
? "config changed while applying this provider update; retry"
|
|
77
|
+
: `config is ${outcome.reason}`);
|
|
71
78
|
}
|
|
72
|
-
|
|
79
|
+
return outcome.value;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
// ---------------------------------------------------------------------------
|
|
@@ -154,19 +161,6 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
154
161
|
const defaultModel = consumeFlagValue(restArgs, "--default-model");
|
|
155
162
|
rejectUnknownArgs(restArgs, ADD_USAGE);
|
|
156
163
|
|
|
157
|
-
const config = loadConfig();
|
|
158
|
-
|
|
159
|
-
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, name);
|
|
160
|
-
if (namespaceCollision) {
|
|
161
|
-
console.error(`Error: ${namespaceCollision}.`);
|
|
162
|
-
process.exit(1);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (hasOwnProvider(config.providers, name) && !force) {
|
|
166
|
-
console.error(`Provider "${name}" already exists. Use --force to overwrite.`);
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
164
|
let provConfig: OcxProviderConfig;
|
|
171
165
|
const registryEntry = getProviderRegistryEntry(name);
|
|
172
166
|
|
|
@@ -211,18 +205,21 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
211
205
|
provConfig.apiKeyTransport = apiKeyTransport;
|
|
212
206
|
}
|
|
213
207
|
|
|
214
|
-
const existingProvider = config.providers[name];
|
|
215
|
-
config.providers[name] = provConfig;
|
|
216
|
-
// A --force overwrite rotates the key/endpoint but must not drop a
|
|
217
|
-
// user-configured price overlay (same rule as the /api/providers path and
|
|
218
|
-
// the login paths); there is no explicit clear/replace flag yet.
|
|
219
|
-
if (existingProvider?.modelCosts !== undefined && provConfig.modelCosts === undefined) {
|
|
220
|
-
provConfig.modelCosts = existingProvider.modelCosts;
|
|
221
|
-
}
|
|
222
208
|
if (allowPrivateNetwork) provConfig.allowPrivateNetwork = true;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
209
|
+
const saved = mutateProviderConfig(config => {
|
|
210
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, name);
|
|
211
|
+
if (namespaceCollision) throw new Error(`${namespaceCollision}.`);
|
|
212
|
+
const existingProvider = config.providers[name];
|
|
213
|
+
if (existingProvider && !force) throw new Error(`Provider "${name}" already exists. Use --force to overwrite.`);
|
|
214
|
+
const next = structuredClone(provConfig);
|
|
215
|
+
// A --force overwrite rotates the key/endpoint but must not drop a user-configured price overlay.
|
|
216
|
+
if (existingProvider?.modelCosts !== undefined && next.modelCosts === undefined) next.modelCosts = existingProvider.modelCosts;
|
|
217
|
+
config.providers[name] = next;
|
|
218
|
+
if (setDefault) config.defaultProvider = name;
|
|
219
|
+
validateProviderConfig(config);
|
|
220
|
+
return { changed: JSON.stringify(existingProvider) !== JSON.stringify(next) || (setDefault && config.defaultProvider === name), value: { provider: next, defaultProvider: config.defaultProvider } };
|
|
221
|
+
});
|
|
222
|
+
provConfig = saved.provider;
|
|
226
223
|
|
|
227
224
|
if (wantsJson) {
|
|
228
225
|
console.log(JSON.stringify({
|
|
@@ -231,7 +228,7 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
231
228
|
adapter: provConfig.adapter,
|
|
232
229
|
baseUrl: provConfig.baseUrl,
|
|
233
230
|
defaultModel: provConfig.defaultModel ?? null,
|
|
234
|
-
isDefault:
|
|
231
|
+
isDefault: saved.defaultProvider === name,
|
|
235
232
|
source: registryEntry ? "registry" : "custom",
|
|
236
233
|
needsSync: true,
|
|
237
234
|
}, null, 2));
|
|
@@ -285,42 +282,32 @@ function handleRemove(args: string[]): void {
|
|
|
285
282
|
}
|
|
286
283
|
rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider remove <name> [--json]");
|
|
287
284
|
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
.map(([id]) => id)
|
|
307
|
-
.sort();
|
|
308
|
-
if (dependentCombos.length > 0) {
|
|
309
|
-
console.error(`Cannot remove "${name}" — combo(s) depend on it: ${dependentCombos.join(", ")}`);
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
delete config.providers[name];
|
|
314
|
-
const droppedCustomModels = dropProviderCustomModels(config, name);
|
|
315
|
-
validateAndSave(config);
|
|
285
|
+
const saved = mutateProviderConfig(config => {
|
|
286
|
+
if (!hasOwnProvider(config.providers, name)) throw new Error(`Provider "${name}" is not configured.`);
|
|
287
|
+
if (name === config.defaultProvider) throw new Error(`Cannot remove "${name}" — it is the default provider. Change the default first: ocx provider set-default <other>`);
|
|
288
|
+
if (Object.keys(config.providers).length <= 1) throw new Error("Cannot remove the last provider.");
|
|
289
|
+
const dependentCombos = Object.entries(config.combos ?? {})
|
|
290
|
+
.filter(([, combo]) => combo.targets.some(target => target.provider === name))
|
|
291
|
+
.map(([id]) => id).sort();
|
|
292
|
+
if (dependentCombos.length > 0) throw new Error(`Cannot remove "${name}" — combo(s) depend on it: ${dependentCombos.join(", ")}`);
|
|
293
|
+
const dependentProfiles = Object.entries(config.routingProfiles ?? {})
|
|
294
|
+
.filter(([, profile]) => profile.candidates.some(candidate => candidate.provider === name))
|
|
295
|
+
.map(([id]) => id).sort();
|
|
296
|
+
if (dependentProfiles.length > 0) throw new Error(`Cannot remove "${name}" — routing profile(s) depend on it: ${dependentProfiles.join(", ")}`);
|
|
297
|
+
delete config.providers[name];
|
|
298
|
+
const droppedCustomModels = dropProviderCustomModels(config, name);
|
|
299
|
+
validateProviderConfig(config);
|
|
300
|
+
return { changed: true, value: { droppedCustomModels, providers: Object.keys(config.providers), defaultProvider: config.defaultProvider } };
|
|
301
|
+
});
|
|
302
|
+
const { droppedCustomModels } = saved;
|
|
316
303
|
|
|
317
304
|
|
|
318
305
|
if (wantsJson) {
|
|
319
306
|
console.log(JSON.stringify({
|
|
320
307
|
action: "removed",
|
|
321
308
|
provider: name,
|
|
322
|
-
remainingProviders:
|
|
323
|
-
defaultProvider:
|
|
309
|
+
remainingProviders: saved.providers,
|
|
310
|
+
defaultProvider: saved.defaultProvider,
|
|
324
311
|
needsSync: true,
|
|
325
312
|
...(droppedCustomModels > 0 ? { droppedCustomModels } : {}),
|
|
326
313
|
}, null, 2));
|
|
@@ -396,13 +383,14 @@ function handleSetDefault(args: string[]): void {
|
|
|
396
383
|
}
|
|
397
384
|
rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider set-default <name> [--json]");
|
|
398
385
|
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
386
|
+
const changed = mutateProviderConfig(config => {
|
|
387
|
+
if (!hasOwnProvider(config.providers, name)) throw new Error(`Provider "${name}" is not configured. Add it first: ocx provider add ${name}`);
|
|
388
|
+
if (config.defaultProvider === name) return { changed: false, value: false };
|
|
389
|
+
config.defaultProvider = name;
|
|
390
|
+
validateProviderConfig(config);
|
|
391
|
+
return { changed: true, value: true };
|
|
392
|
+
});
|
|
393
|
+
if (!changed) {
|
|
406
394
|
if (wantsJson) {
|
|
407
395
|
console.log(JSON.stringify({ action: "noop", provider: name, defaultProvider: name, needsSync: false }, null, 2));
|
|
408
396
|
} else {
|
|
@@ -411,10 +399,6 @@ function handleSetDefault(args: string[]): void {
|
|
|
411
399
|
return;
|
|
412
400
|
}
|
|
413
401
|
|
|
414
|
-
config.defaultProvider = name;
|
|
415
|
-
validateAndSave(config);
|
|
416
|
-
|
|
417
|
-
|
|
418
402
|
if (wantsJson) {
|
|
419
403
|
console.log(JSON.stringify({ action: "set-default", provider: name, defaultProvider: name, needsSync: true }, null, 2));
|
|
420
404
|
return;
|
package/src/cli/registry.ts
CHANGED
|
@@ -205,7 +205,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
207
|
name: "agent",
|
|
208
|
-
usage: "ocx agent <status|injection|effort|subagents|roles|fallback|sidecar> ...",
|
|
208
|
+
usage: "ocx agent <status|injection|effort|subagents|authority|roles|fallback|sidecar> ...",
|
|
209
209
|
summary: "Manage headless multi-agent, roster, roles, effort, injection, and sidecar settings.",
|
|
210
210
|
},
|
|
211
211
|
{
|
package/src/codex/catalog.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, c
|
|
|
8
8
|
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
|
|
9
9
|
export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
|
|
10
10
|
export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
|
|
11
|
-
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache, finalizeAutoReviewModelOverride } from "./catalog/sync";
|
|
11
|
+
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, isEligibleV2SubagentEntry, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache, finalizeAutoReviewModelOverride } from "./catalog/sync";
|
|
12
12
|
export type { ObservedCatalogMergeInput } from "./catalog/sync";
|
|
13
13
|
export type { SpawnAgentSurface, SubagentRosterExclusionReason, EffectiveSubagentModel, SubagentRosterExclusion, EffectiveSubagentRoster } from "./catalog/sync";
|
|
14
14
|
export { accountBoundNativeDisplayName, accountBoundNativeModelSlugs, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./catalog/account-models";
|
package/src/codex/convergence.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
|
-
import { getConfigDir,
|
|
4
|
-
import {
|
|
3
|
+
import { getConfigDir, mutatePersistedConfig, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
4
|
+
import {
|
|
5
|
+
reconcileSuccessfulModelDiscoveries,
|
|
6
|
+
type KnownModelBaseline,
|
|
7
|
+
} from "../providers/new-model-policy";
|
|
5
8
|
import { COMBO_NAMESPACE } from "../combos";
|
|
6
9
|
import { getAuthStorePath } from "../oauth/store";
|
|
7
|
-
import type { OcxConfig } from "../types";
|
|
10
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
8
11
|
import { captureCatalogAdmissionSnapshot } from "./catalog-admission";
|
|
9
12
|
import { legacyCustomModelCatalogSlugs } from "./custom-model-catalog-migration";
|
|
10
13
|
import {
|
|
@@ -137,7 +140,22 @@ interface CandidateState {
|
|
|
137
140
|
readonly changed: boolean;
|
|
138
141
|
readonly notices: readonly CatalogNotice[];
|
|
139
142
|
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
140
|
-
readonly
|
|
143
|
+
readonly discovery: DiscoveryEvidence;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface DiscoveryEvidence {
|
|
147
|
+
readonly models: readonly { provider: string; id: string; custom?: boolean }[];
|
|
148
|
+
readonly authoritativeProviders: readonly string[];
|
|
149
|
+
readonly providerIdentities: Readonly<Record<string, string>>;
|
|
150
|
+
readonly now: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface DiscoveryProjection {
|
|
154
|
+
readonly providers: Readonly<Record<string, Readonly<{
|
|
155
|
+
knownModel?: KnownModelBaseline;
|
|
156
|
+
recentArrivals?: readonly { id: string; at: string }[];
|
|
157
|
+
}>>>;
|
|
158
|
+
readonly disabledModelsToAdd: readonly string[];
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
const candidateStates = new WeakMap<object, CandidateState>();
|
|
@@ -145,6 +163,16 @@ function same(left: unknown, right: unknown): boolean {
|
|
|
145
163
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
146
164
|
}
|
|
147
165
|
|
|
166
|
+
function discoveryProviderIdentity(provider: OcxProviderConfig | undefined): string {
|
|
167
|
+
if (!provider) return "missing";
|
|
168
|
+
const row: Record<string, unknown> = { ...provider };
|
|
169
|
+
delete row.note;
|
|
170
|
+
delete row.newModelPolicy;
|
|
171
|
+
return JSON.stringify(row, (_key, value) => value && typeof value === "object" && !Array.isArray(value)
|
|
172
|
+
? Object.fromEntries(Object.entries(value as Record<string, unknown>).sort(([left], [right]) => left.localeCompare(right)))
|
|
173
|
+
: value);
|
|
174
|
+
}
|
|
175
|
+
|
|
148
176
|
function targetPath(identity: string): string {
|
|
149
177
|
const parsed = JSON.parse(identity) as { path?: unknown };
|
|
150
178
|
if (typeof parsed.path !== "string") throw new TypeError("Catalog target identity has no path.");
|
|
@@ -441,13 +469,15 @@ export async function gatherCodexCatalogCandidate(
|
|
|
441
469
|
: !hasRoutedEntries(source.catalog) ? source.catalog : null)
|
|
442
470
|
: null);
|
|
443
471
|
const discoveryConfig = structuredClone(snapshot.config) as OcxConfig;
|
|
444
|
-
const
|
|
472
|
+
const authoritativeProviders = providerModelOutcomes
|
|
473
|
+
.filter(outcome => outcome.state === "authoritative")
|
|
474
|
+
.map(outcome => outcome.provider);
|
|
475
|
+
const discoveryNow = new Date().toISOString();
|
|
476
|
+
reconcileSuccessfulModelDiscoveries({
|
|
445
477
|
config: discoveryConfig,
|
|
446
478
|
models: routedModels,
|
|
447
|
-
authoritativeProviders
|
|
448
|
-
|
|
449
|
-
.map(outcome => outcome.provider),
|
|
450
|
-
now: new Date().toISOString(),
|
|
479
|
+
authoritativeProviders,
|
|
480
|
+
now: discoveryNow,
|
|
451
481
|
});
|
|
452
482
|
const preparedCatalog = prepareCatalog(
|
|
453
483
|
discoveryConfig,
|
|
@@ -515,7 +545,15 @@ export async function gatherCodexCatalogCandidate(
|
|
|
515
545
|
|| Buffer.from(cacheBytes ?? []).toString("utf8") !== preparedCacheBytes,
|
|
516
546
|
notices: Object.freeze([...notices]),
|
|
517
547
|
modelEntitlements,
|
|
518
|
-
|
|
548
|
+
discovery: {
|
|
549
|
+
models: routedModels,
|
|
550
|
+
authoritativeProviders,
|
|
551
|
+
providerIdentities: Object.fromEntries(authoritativeProviders.map(provider => [
|
|
552
|
+
provider,
|
|
553
|
+
discoveryProviderIdentity(snapshot.config.providers[provider]),
|
|
554
|
+
])),
|
|
555
|
+
now: discoveryNow,
|
|
556
|
+
},
|
|
519
557
|
});
|
|
520
558
|
return { kind: "candidate", candidate };
|
|
521
559
|
} catch (error) {
|
|
@@ -532,6 +570,63 @@ export async function gatherCodexCatalogCandidate(
|
|
|
532
570
|
}
|
|
533
571
|
}
|
|
534
572
|
|
|
573
|
+
function reconcilePersistedDiscovery(state: CandidateState) {
|
|
574
|
+
return mutatePersistedConfig(config => {
|
|
575
|
+
if (state.discovery.authoritativeProviders.some(provider => (
|
|
576
|
+
discoveryProviderIdentity(config.providers[provider]) !== state.discovery.providerIdentities[provider]
|
|
577
|
+
))) {
|
|
578
|
+
return { changed: false, value: null };
|
|
579
|
+
}
|
|
580
|
+
const disabledBefore = new Set(config.disabledModels ?? []);
|
|
581
|
+
const changed = reconcileSuccessfulModelDiscoveries({
|
|
582
|
+
config,
|
|
583
|
+
...state.discovery,
|
|
584
|
+
});
|
|
585
|
+
const providers = Object.fromEntries(state.discovery.authoritativeProviders.flatMap(provider => {
|
|
586
|
+
const configured = config.providers[provider];
|
|
587
|
+
if (!configured || configured.liveModels === false) return [];
|
|
588
|
+
const knownModel = config.modelDiscovery?.knownModels?.[provider];
|
|
589
|
+
const recentArrivals = config.modelDiscovery?.recentArrivals?.[provider];
|
|
590
|
+
return [[provider, {
|
|
591
|
+
...(knownModel ? { knownModel: structuredClone(knownModel) } : {}),
|
|
592
|
+
...(recentArrivals ? { recentArrivals: structuredClone(recentArrivals) } : {}),
|
|
593
|
+
}]];
|
|
594
|
+
}));
|
|
595
|
+
return {
|
|
596
|
+
changed,
|
|
597
|
+
value: {
|
|
598
|
+
providers,
|
|
599
|
+
disabledModelsToAdd: (config.disabledModels ?? []).filter(slug => !disabledBefore.has(slug)),
|
|
600
|
+
} satisfies DiscoveryProjection,
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function adoptDiscoveryProjection(config: OcxConfig, projection: DiscoveryProjection): void {
|
|
606
|
+
for (const [provider, discovered] of Object.entries(projection.providers)) {
|
|
607
|
+
if (discovered.knownModel) {
|
|
608
|
+
const discovery = config.modelDiscovery ??= {};
|
|
609
|
+
const knownModels = discovery.knownModels ??= {};
|
|
610
|
+
knownModels[provider] = structuredClone(discovered.knownModel);
|
|
611
|
+
} else if (config.modelDiscovery?.knownModels) {
|
|
612
|
+
delete config.modelDiscovery.knownModels[provider];
|
|
613
|
+
}
|
|
614
|
+
if (discovered.recentArrivals) {
|
|
615
|
+
const discovery = config.modelDiscovery ??= {};
|
|
616
|
+
const recentArrivals = discovery.recentArrivals ??= {};
|
|
617
|
+
recentArrivals[provider] = discovered.recentArrivals.map(arrival => ({ ...arrival }));
|
|
618
|
+
} else if (config.modelDiscovery?.recentArrivals) {
|
|
619
|
+
delete config.modelDiscovery.recentArrivals[provider];
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (projection.disabledModelsToAdd.length > 0) {
|
|
623
|
+
const disabledModels = config.disabledModels ??= [];
|
|
624
|
+
for (const slug of projection.disabledModelsToAdd) {
|
|
625
|
+
if (!disabledModels.includes(slug)) disabledModels.push(slug);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
535
630
|
function revalidateCandidate(state: CandidateState): CodexCatalogCommitResult | null {
|
|
536
631
|
if (!isCodexModelEntitlementSnapshotCurrent(state.modelEntitlements)) {
|
|
537
632
|
return { kind: "stale", reason: "account-entitlement" };
|
|
@@ -663,11 +758,11 @@ export async function convergeCodexCatalog(
|
|
|
663
758
|
const state = candidateStates.get(gathered.candidate as object)!;
|
|
664
759
|
lifecycle.onCommitBegin?.();
|
|
665
760
|
const committed = await commitCodexCatalogCandidate(gathered.candidate, request.deadlineMs);
|
|
666
|
-
if (committed.kind === "committed"
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
761
|
+
if (committed.kind === "committed") {
|
|
762
|
+
const persisted = reconcilePersistedDiscovery(state);
|
|
763
|
+
if ((persisted.status === "committed" || persisted.status === "unchanged") && persisted.value) {
|
|
764
|
+
adoptDiscoveryProjection(snapshot.config as OcxConfig, persisted.value);
|
|
765
|
+
}
|
|
671
766
|
}
|
|
672
767
|
return {
|
|
673
768
|
changed: committed.kind === "committed" ? committed.changed : false,
|
package/src/codex/inject.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
writeJournal,
|
|
39
39
|
} from "./journal";
|
|
40
40
|
import { withCatalogWriteSerialization } from "./catalog-write-serialization";
|
|
41
|
+
import { resetCodexAppServerCatalogStateCache } from "./app-server-processes";
|
|
41
42
|
import { restoreCodexCatalogWithPermit } from "./catalog/sync";
|
|
42
43
|
import { syncCodexHistoryProvider, type CodexHistoryFailureReason } from "./history-provider";
|
|
43
44
|
import {
|
|
@@ -931,6 +932,7 @@ export async function injectCodexConfig(
|
|
|
931
932
|
configContent: baselineContent,
|
|
932
933
|
});
|
|
933
934
|
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
935
|
+
resetCodexAppServerCatalogStateCache();
|
|
934
936
|
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
935
937
|
markJournalInjectedState(content, profileContent, {
|
|
936
938
|
// A root override is ours only in loopback Design B when no user-owned value won.
|
|
@@ -442,16 +442,9 @@ export async function resolveNativeDefaultState(
|
|
|
442
442
|
const inspected = inspectManagedSubagentDefaults(content);
|
|
443
443
|
if (!inspected.ok) return "blocked";
|
|
444
444
|
|
|
445
|
-
let parsed: Record<string, unknown>;
|
|
446
|
-
try {
|
|
447
|
-
parsed = Bun.TOML.parse(content) as Record<string, unknown>;
|
|
448
|
-
} catch {
|
|
449
|
-
return "blocked";
|
|
450
|
-
}
|
|
451
445
|
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
452
446
|
if (provider && provider !== "openai" && provider !== "opencodex") return "blocked";
|
|
453
|
-
|
|
454
|
-
if (typeof baseUrl === "string" && baseUrl.trim() && !hasInjectedCodexRouting(content)) return "blocked";
|
|
447
|
+
if (!hasInjectedCodexRouting(content)) return "blocked";
|
|
455
448
|
|
|
456
449
|
const { values, owned } = inspected.inspection;
|
|
457
450
|
for (const key of TARGET_KEYS) {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { isEligibleV2SubagentEntry, readCatalog, readCodexCatalogPath } from "./catalog";
|
|
3
|
+
import { catalogEntryEfforts } from "./catalog/effort";
|
|
4
|
+
import type { RawEntry } from "./catalog/parsing";
|
|
5
|
+
import { resolveNativeDefaultState, type NativeDefaultState } from "./subagent-defaults";
|
|
6
|
+
|
|
7
|
+
export const SUBAGENT_MODEL_AUTHORITY_VERSION = 1 as const;
|
|
8
|
+
|
|
9
|
+
export interface SubagentModelAuthorityInput {
|
|
10
|
+
schemaVersion: typeof SUBAGENT_MODEL_AUTHORITY_VERSION;
|
|
11
|
+
role: string;
|
|
12
|
+
agentType: string;
|
|
13
|
+
typedDispatchRequired?: boolean;
|
|
14
|
+
requestedModel?: string;
|
|
15
|
+
requestedEffort?: string;
|
|
16
|
+
spawnAgent: {
|
|
17
|
+
surface: "v1" | "v2";
|
|
18
|
+
supportsAgentType: boolean;
|
|
19
|
+
supportsModel: boolean;
|
|
20
|
+
supportsEffort: boolean;
|
|
21
|
+
supportsForkTurns: boolean;
|
|
22
|
+
};
|
|
23
|
+
confirmation?: { decision: "approve" | "decline" };
|
|
24
|
+
interactive?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SubagentModelAuthorityHost {
|
|
28
|
+
catalogState: "fresh" | "stale" | "not_running" | "unknown";
|
|
29
|
+
nativeDefaultState: NativeDefaultState;
|
|
30
|
+
preferredModel: string | null;
|
|
31
|
+
preferredEffort: string | null;
|
|
32
|
+
executableModels: Array<{ model: string; efforts: string[] }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SubagentModelAuthorityResult = {
|
|
36
|
+
schemaVersion: typeof SUBAGENT_MODEL_AUTHORITY_VERSION;
|
|
37
|
+
decision: "forward" | "omit" | "confirm" | "blocked";
|
|
38
|
+
requestClassification: "inherit" | "preferred" | "exception";
|
|
39
|
+
reason: string;
|
|
40
|
+
spawn?: {
|
|
41
|
+
agent_type?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
reasoning_effort?: string;
|
|
44
|
+
fork_turns?: "none";
|
|
45
|
+
};
|
|
46
|
+
confirmation?: {
|
|
47
|
+
role: string;
|
|
48
|
+
preferredModel: string | null;
|
|
49
|
+
requestedModel: string;
|
|
50
|
+
scope: "single spawn";
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function parseSubagentModelAuthorityInput(value: unknown): SubagentModelAuthorityInput | null {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
56
|
+
const input = value as Record<string, unknown>;
|
|
57
|
+
const spawn = input.spawnAgent;
|
|
58
|
+
if (!spawn || typeof spawn !== "object" || Array.isArray(spawn)) return null;
|
|
59
|
+
const tool = spawn as Record<string, unknown>;
|
|
60
|
+
if (input.schemaVersion !== SUBAGENT_MODEL_AUTHORITY_VERSION
|
|
61
|
+
|| typeof input.role !== "string"
|
|
62
|
+
|| typeof input.agentType !== "string"
|
|
63
|
+
|| (input.typedDispatchRequired !== undefined && typeof input.typedDispatchRequired !== "boolean")
|
|
64
|
+
|| (input.requestedModel !== undefined && typeof input.requestedModel !== "string")
|
|
65
|
+
|| (input.requestedEffort !== undefined && typeof input.requestedEffort !== "string")
|
|
66
|
+
|| (input.interactive !== undefined && typeof input.interactive !== "boolean")
|
|
67
|
+
|| (tool.surface !== "v1" && tool.surface !== "v2")
|
|
68
|
+
|| ["supportsAgentType", "supportsModel", "supportsEffort", "supportsForkTurns"]
|
|
69
|
+
.some(key => typeof tool[key] !== "boolean")) return null;
|
|
70
|
+
if (input.confirmation !== undefined) {
|
|
71
|
+
if (!input.confirmation || typeof input.confirmation !== "object" || Array.isArray(input.confirmation)) return null;
|
|
72
|
+
const decision = (input.confirmation as Record<string, unknown>).decision;
|
|
73
|
+
if (decision !== "approve" && decision !== "decline") return null;
|
|
74
|
+
}
|
|
75
|
+
return value as SubagentModelAuthorityInput;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function result(
|
|
79
|
+
decision: SubagentModelAuthorityResult["decision"],
|
|
80
|
+
requestClassification: SubagentModelAuthorityResult["requestClassification"],
|
|
81
|
+
reason: string,
|
|
82
|
+
extra: Pick<SubagentModelAuthorityResult, "spawn" | "confirmation"> = {},
|
|
83
|
+
): SubagentModelAuthorityResult {
|
|
84
|
+
return { schemaVersion: SUBAGENT_MODEL_AUTHORITY_VERSION, decision, requestClassification, reason, ...extra };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function resolveSubagentModelAuthority(
|
|
88
|
+
input: SubagentModelAuthorityInput,
|
|
89
|
+
host: SubagentModelAuthorityHost,
|
|
90
|
+
): SubagentModelAuthorityResult {
|
|
91
|
+
const requestedModel = input.requestedModel?.trim() || undefined;
|
|
92
|
+
const preferredModel = host.preferredModel?.trim() || undefined;
|
|
93
|
+
const classification = requestedModel === undefined
|
|
94
|
+
? "inherit"
|
|
95
|
+
: requestedModel === preferredModel ? "preferred" : "exception";
|
|
96
|
+
if (input.schemaVersion !== SUBAGENT_MODEL_AUTHORITY_VERSION) {
|
|
97
|
+
return result("blocked", classification, "unsupported authority schema version");
|
|
98
|
+
}
|
|
99
|
+
if (!input.role.trim() || !input.agentType.trim()) {
|
|
100
|
+
return result("blocked", classification, "role and agentType must be non-empty");
|
|
101
|
+
}
|
|
102
|
+
if (input.typedDispatchRequired && !input.spawnAgent.supportsAgentType) {
|
|
103
|
+
return result("blocked", classification, "spawn_agent does not support required agent_type dispatch");
|
|
104
|
+
}
|
|
105
|
+
if (host.catalogState === "stale" || host.catalogState === "unknown") {
|
|
106
|
+
return result("blocked", classification, "OpenCodex model authority is not current; sync or restart Codex and retry");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const chosenModel = requestedModel ?? preferredModel;
|
|
110
|
+
const executable = chosenModel
|
|
111
|
+
? host.executableModels.find(candidate => candidate.model === chosenModel)
|
|
112
|
+
: undefined;
|
|
113
|
+
if (chosenModel && !executable) {
|
|
114
|
+
return result("blocked", classification, `model ${JSON.stringify(chosenModel)} is not executable on this collaboration surface`);
|
|
115
|
+
}
|
|
116
|
+
if (chosenModel && !input.spawnAgent.supportsModel) {
|
|
117
|
+
return result("blocked", classification, "spawn_agent does not support model overrides");
|
|
118
|
+
}
|
|
119
|
+
const effort = input.requestedEffort?.trim() || (requestedModel ? undefined : host.preferredEffort?.trim()) || undefined;
|
|
120
|
+
if (effort && !input.spawnAgent.supportsEffort) {
|
|
121
|
+
return result("blocked", classification, "spawn_agent does not support reasoning_effort overrides");
|
|
122
|
+
}
|
|
123
|
+
if (effort && executable && !executable.efforts.includes(effort)) {
|
|
124
|
+
return result("blocked", classification, `reasoning effort ${JSON.stringify(effort)} is unavailable for ${JSON.stringify(chosenModel)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (classification === "exception") {
|
|
128
|
+
if (input.confirmation?.decision === "decline") {
|
|
129
|
+
return result("blocked", classification, "single-spawn model exception was declined");
|
|
130
|
+
}
|
|
131
|
+
if (input.confirmation?.decision !== "approve") {
|
|
132
|
+
if (input.interactive === false) {
|
|
133
|
+
return result("blocked", classification, "single-spawn model exception requires interactive confirmation");
|
|
134
|
+
}
|
|
135
|
+
return result("confirm", classification, "confirm this model exception for one spawn", {
|
|
136
|
+
confirmation: {
|
|
137
|
+
role: input.role,
|
|
138
|
+
preferredModel: preferredModel ?? null,
|
|
139
|
+
requestedModel: requestedModel!,
|
|
140
|
+
scope: "single spawn",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!chosenModel) {
|
|
147
|
+
return host.nativeDefaultState === "active"
|
|
148
|
+
? result("omit", classification, "native OpenCodex defaults are active")
|
|
149
|
+
: result("blocked", classification, `native OpenCodex defaults are ${host.nativeDefaultState}`);
|
|
150
|
+
}
|
|
151
|
+
const spawn: NonNullable<SubagentModelAuthorityResult["spawn"]> = {
|
|
152
|
+
...(input.spawnAgent.supportsAgentType ? { agent_type: input.agentType } : {}),
|
|
153
|
+
model: chosenModel,
|
|
154
|
+
...(effort ? { reasoning_effort: effort } : {}),
|
|
155
|
+
...((input.spawnAgent.supportsForkTurns && (chosenModel || effort)) ? { fork_turns: "none" as const } : {}),
|
|
156
|
+
};
|
|
157
|
+
return result("forward", classification, classification === "exception"
|
|
158
|
+
? "single-spawn model exception approved"
|
|
159
|
+
: "OpenCodex preferred model is executable", { spawn });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function executableModels(entries: readonly RawEntry[], surface: "v1" | "v2"): SubagentModelAuthorityHost["executableModels"] {
|
|
163
|
+
const seen = new Set<string>();
|
|
164
|
+
return entries.flatMap(entry => {
|
|
165
|
+
const model = typeof entry.slug === "string" ? entry.slug.trim() : "";
|
|
166
|
+
if (!model || seen.has(model) || entry.visibility !== "list" || (surface === "v2" && !isEligibleV2SubagentEntry(entry))) return [];
|
|
167
|
+
seen.add(model);
|
|
168
|
+
return [{ model, efforts: catalogEntryEfforts(entry) }];
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function resolveOpenCodexSubagentModelAuthority(
|
|
173
|
+
input: SubagentModelAuthorityInput,
|
|
174
|
+
config: OcxConfig,
|
|
175
|
+
): Promise<SubagentModelAuthorityResult> {
|
|
176
|
+
const { collectCodexAppServerCatalogStateForRequest } = await import("./app-server-processes");
|
|
177
|
+
const catalogState = await collectCodexAppServerCatalogStateForRequest();
|
|
178
|
+
const models = executableModels(readCatalog(readCodexCatalogPath())?.models ?? [], input.spawnAgent.surface);
|
|
179
|
+
const configured = config.injectionModel?.trim() || null;
|
|
180
|
+
const exact = configured ? models.find(candidate => candidate.model === configured)?.model ?? null : null;
|
|
181
|
+
const host: SubagentModelAuthorityHost = {
|
|
182
|
+
catalogState: catalogState.state,
|
|
183
|
+
nativeDefaultState: await resolveNativeDefaultState(config),
|
|
184
|
+
preferredModel: exact,
|
|
185
|
+
preferredEffort: config.injectionEffort?.trim() || null,
|
|
186
|
+
executableModels: models,
|
|
187
|
+
};
|
|
188
|
+
return resolveSubagentModelAuthority(input, host);
|
|
189
|
+
}
|