@bitkyc08/opencodex 2.63.0 → 2.64.0
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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-CpuDF3ci.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-B4uEIa1O.js} +1 -1
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/index-SggB6t3z.js +86 -0
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { projectCustomModelCatalogMigration } from "../codex/custom-model-catalog-migration";
|
|
3
|
+
import { readConfigFileSnapshot, type ConfigFileSnapshot } from "./diagnostics";
|
|
4
|
+
import { bumpGenerationForCooperatingConfigWrite, withConfigMutationLockSync } from "./mutation-lock";
|
|
5
|
+
import { persistConfigUnlocked } from "./persist-unlocked";
|
|
6
|
+
import { projectConfigRebaseProvenance } from "./rebase-provenance";
|
|
7
|
+
|
|
8
|
+
export type PersistedConfigMutation<T> = {
|
|
9
|
+
changed: boolean;
|
|
10
|
+
value: T;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type PersistedConfigMutationOutcome<T> =
|
|
14
|
+
| { status: "committed" | "unchanged"; value: T }
|
|
15
|
+
| { status: "unavailable"; reason: "missing" | "invalid" | "conflict" };
|
|
16
|
+
|
|
17
|
+
const CONFIG_MUTATION_MAX_REBASE_ATTEMPTS = 3;
|
|
18
|
+
let persistedConfigMutationBeforeCommitForTests: (() => void) | null = null;
|
|
19
|
+
|
|
20
|
+
/** Test-only one-shot seam: inject a competing mutation after the first decision, before freshness revalidation. */
|
|
21
|
+
export function setPersistedConfigMutationBeforeCommitForTests(hook: (() => void) | null): void {
|
|
22
|
+
persistedConfigMutationBeforeCommitForTests = hook;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function unavailableConfigMutationReason(snapshot: ConfigFileSnapshot): "missing" | "invalid" {
|
|
26
|
+
return snapshot.diagnostics.source === "default" ? "missing" : "invalid";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Patch a schema-valid on-disk config under the shared mutation lock. Cooperating writers are
|
|
31
|
+
* serialized; the callback is rerun on the newest snapshot so observed direct byte changes rebase
|
|
32
|
+
* and credential predicates are re-evaluated immediately before the atomic commit. A writer that
|
|
33
|
+
* ignores the coordinator can still change bytes after the final check because the filesystem has
|
|
34
|
+
* no portable conditional rename. Missing or malformed config always fails closed and is never
|
|
35
|
+
* recreated from a prior snapshot.
|
|
36
|
+
*/
|
|
37
|
+
export function mutatePersistedConfig<T>(
|
|
38
|
+
mutate: (config: OcxConfig) => PersistedConfigMutation<T>,
|
|
39
|
+
): PersistedConfigMutationOutcome<T> {
|
|
40
|
+
// Avoid creating/opening the coordinator database for a read-path update that already knows
|
|
41
|
+
// there is no valid config. The same check runs again under the transaction for authority.
|
|
42
|
+
const observed = readConfigFileSnapshot();
|
|
43
|
+
if (observed.diagnostics.source !== "file" || observed.raw === undefined) {
|
|
44
|
+
return { status: "unavailable", reason: unavailableConfigMutationReason(observed) };
|
|
45
|
+
}
|
|
46
|
+
return withConfigMutationLockSync(() => {
|
|
47
|
+
let base = readConfigFileSnapshot();
|
|
48
|
+
for (let attempt = 0; attempt < CONFIG_MUTATION_MAX_REBASE_ATTEMPTS; attempt += 1) {
|
|
49
|
+
if (base.diagnostics.source !== "file" || base.raw === undefined) {
|
|
50
|
+
return { status: "unavailable", reason: unavailableConfigMutationReason(base) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tentativeConfig = structuredClone(base.diagnostics.config);
|
|
54
|
+
const tentative = mutate(tentativeConfig);
|
|
55
|
+
if (!tentative.changed) return { status: "unchanged", value: tentative.value };
|
|
56
|
+
|
|
57
|
+
const hook = persistedConfigMutationBeforeCommitForTests;
|
|
58
|
+
persistedConfigMutationBeforeCommitForTests = null;
|
|
59
|
+
hook?.();
|
|
60
|
+
|
|
61
|
+
const latest = readConfigFileSnapshot();
|
|
62
|
+
if (latest.diagnostics.source !== "file" || latest.raw === undefined) {
|
|
63
|
+
return { status: "unavailable", reason: unavailableConfigMutationReason(latest) };
|
|
64
|
+
}
|
|
65
|
+
if (latest.raw !== base.raw) {
|
|
66
|
+
base = latest;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Re-run against a fresh clone even when config bytes are unchanged: a Codex credential
|
|
71
|
+
// generation lives in a separate file and may have changed at the injected seam.
|
|
72
|
+
const confirmedConfig = structuredClone(latest.diagnostics.config);
|
|
73
|
+
const confirmed = mutate(confirmedConfig);
|
|
74
|
+
if (!confirmed.changed) return { status: "unchanged", value: confirmed.value };
|
|
75
|
+
|
|
76
|
+
const commitBase = readConfigFileSnapshot();
|
|
77
|
+
if (commitBase.diagnostics.source !== "file" || commitBase.raw === undefined) {
|
|
78
|
+
return { status: "unavailable", reason: unavailableConfigMutationReason(commitBase) };
|
|
79
|
+
}
|
|
80
|
+
if (commitBase.raw !== latest.raw) {
|
|
81
|
+
base = commitBase;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const projected = projectCustomModelCatalogMigration(
|
|
86
|
+
commitBase.diagnostics.config,
|
|
87
|
+
projectConfigRebaseProvenance(confirmedConfig),
|
|
88
|
+
);
|
|
89
|
+
if (persistConfigUnlocked(projected)) bumpGenerationForCooperatingConfigWrite();
|
|
90
|
+
return { status: "committed", value: confirmed.value };
|
|
91
|
+
}
|
|
92
|
+
return { status: "unavailable", reason: "conflict" };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -370,7 +370,7 @@ export function modelAdapterRecordConfigError(
|
|
|
370
370
|
if (typeof entry !== "string" || !MODEL_ADAPTER_OVERRIDE_ALLOWED.has(entry)) {
|
|
371
371
|
return `${field}.${key} must be one of: ${[...MODEL_ADAPTER_OVERRIDE_ALLOWED].join(", ")}`;
|
|
372
372
|
}
|
|
373
|
-
if (isWirePinnedModel(providerName, key.trim())) {
|
|
373
|
+
if (isWirePinnedModel(providerName, key.trim(), provider)) {
|
|
374
374
|
return `${field}.${key} cannot be overridden: the upstream only speaks one wire for this model`;
|
|
375
375
|
}
|
|
376
376
|
}
|
package/src/config/proxy-env.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { configureSocks5Fetch } from "../lib/proxy-env";
|
|
1
|
+
import { configureSocks5Fetch, socks5ProxyFromEnv } from "../lib/proxy-env";
|
|
2
2
|
import { redactUrlForLog } from "../lib/redact";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DEFAULT_SUBAGENT_MODELS, SUBAGENT_MODELS_VERSION } from "./subagent-models";
|
|
@@ -98,6 +98,60 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
const LOOPBACK_NO_PROXY = ["localhost", "127.0.0.1", "::1", "[::1]"] as const;
|
|
102
|
+
const LOOPBACK_ADDRESS_NO_PROXY = ["127.0.0.1", "::1", "[::1]"] as const;
|
|
103
|
+
|
|
104
|
+
// With no config.proxy, which loopback bypasses are written depends on who reads them. The
|
|
105
|
+
// installed SOCKS fetch wrapper (src/lib/proxy-env.ts configuredOutboundFetch) matches these
|
|
106
|
+
// entries as exact hosts. Bun applies an inherited HTTP(S) proxy itself and matches NO_PROXY
|
|
107
|
+
// entries as domain suffixes, so a bare "localhost" there would also send any *.localhost name
|
|
108
|
+
// direct, including from a fetch that never passes the wrapper. The full list is therefore
|
|
109
|
+
// written only when an inherited SOCKS proxy is the only one. Whenever Bun applies an inherited
|
|
110
|
+
// HTTP(S) proxy, only the loopback addresses are added: they cannot widen that way (a URL host
|
|
111
|
+
// ending in a numeric label parses as IPv4) and keep local health and management calls to
|
|
112
|
+
// 127.0.0.1 off the proxy. A proxy-free process is left untouched: writing NO_PROXY into it is
|
|
113
|
+
// itself a proxy-env mutation callers observe (the lab sandbox rejects these keys).
|
|
114
|
+
function inheritedLoopbackBypass(): readonly string[] | undefined {
|
|
115
|
+
const schemeProxy = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"].some(key => process.env[key]?.trim());
|
|
116
|
+
const httpAllProxy = ["ALL_PROXY", "all_proxy"].some(key => {
|
|
117
|
+
const value = process.env[key]?.trim();
|
|
118
|
+
if (!value) return false;
|
|
119
|
+
try {
|
|
120
|
+
return ["http:", "https:"].includes(new URL(value).protocol);
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (schemeProxy || httpAllProxy) return LOOPBACK_ADDRESS_NO_PROXY;
|
|
126
|
+
return socks5ProxyFromEnv() !== undefined ? LOOPBACK_NO_PROXY : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function withNoProxyEntries(existing: string, configured: readonly string[], loopback: readonly string[]): string {
|
|
130
|
+
const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
|
|
131
|
+
const seen = new Set(entries.map(entry => entry.toLowerCase()));
|
|
132
|
+
for (const host of [...configured, ...loopback]) {
|
|
133
|
+
const key = host.toLowerCase();
|
|
134
|
+
if (!seen.has(key)) {
|
|
135
|
+
entries.push(host);
|
|
136
|
+
seen.add(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return entries.join(",");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function mergeNoProxyEntries(configured: readonly string[] = [], loopback: readonly string[] = LOOPBACK_NO_PROXY): void {
|
|
143
|
+
process.env.NO_PROXY = withNoProxyEntries(process.env.NO_PROXY ?? process.env.no_proxy ?? "", configured, loopback);
|
|
144
|
+
// Bun's native fetch reads a non-empty lowercase no_proxy before NO_PROXY
|
|
145
|
+
// (src/codex/catalog/remote.ts), so an inherited one would shadow the loopback entries above.
|
|
146
|
+
// Only the loopback addresses join it: Bun matches entries as domain suffixes, and any name
|
|
147
|
+
// (a bare "localhost", or a configured noProxy entry the inherited value always shadowed)
|
|
148
|
+
// would send its subdomains past a proxy the inherited value kept them on.
|
|
149
|
+
const inherited = process.env.no_proxy;
|
|
150
|
+
if (inherited !== undefined && inherited.trim() !== "") {
|
|
151
|
+
process.env.no_proxy = withNoProxyEntries(inherited, [], loopback.filter(host => host !== "localhost"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
101
155
|
/**
|
|
102
156
|
* Mirror `config.proxy` into HTTP(S)_PROXY env vars. Bun fetch consumes them natively; transports
|
|
103
157
|
* such as the ChatGPT upstream WebSocket select the same environment explicitly. User-set HTTP(S)_PROXY
|
|
@@ -130,6 +184,12 @@ export function applyProxyEnvWith(
|
|
|
130
184
|
let proxy = typeof rawProxy === "string" ? resolveEnvValue(rawProxy) : undefined;
|
|
131
185
|
if (!proxy) {
|
|
132
186
|
if (rawProxy !== undefined) warnProxyConfigDiscardOnce("proxy");
|
|
187
|
+
// Inherited-SOCKS path: only loopback bypasses are appended. A configured noProxy is
|
|
188
|
+
// deliberately NOT merged here — with no config.proxy the operator's bypass list has
|
|
189
|
+
// no declared proxy to apply against, and merging it would silently widen direct
|
|
190
|
+
// egress beyond the loopback fix this branch exists for.
|
|
191
|
+
const loopback = inheritedLoopbackBypass();
|
|
192
|
+
if (loopback) mergeNoProxyEntries([], loopback);
|
|
133
193
|
configureSocks5Fetch();
|
|
134
194
|
return;
|
|
135
195
|
}
|
|
@@ -178,9 +238,6 @@ export function applyProxyEnvWith(
|
|
|
178
238
|
if (!process.env.HTTPS_PROXY?.trim() && !process.env.https_proxy?.trim()) process.env.HTTPS_PROXY = proxy;
|
|
179
239
|
}
|
|
180
240
|
}
|
|
181
|
-
const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
182
|
-
const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
|
|
183
|
-
const seen = new Set(entries.map(e => e.toLowerCase()));
|
|
184
241
|
// Configured entries first, then loopback: loopback is unconditional, so appending it last
|
|
185
242
|
// keeps it present even when the operator lists a loopback host themselves.
|
|
186
243
|
const raw = config.noProxy;
|
|
@@ -200,13 +257,6 @@ export function applyProxyEnvWith(
|
|
|
200
257
|
const configured = configuredEntries
|
|
201
258
|
.map(entry => entry.trim())
|
|
202
259
|
.filter(Boolean);
|
|
203
|
-
|
|
204
|
-
const key = host.toLowerCase();
|
|
205
|
-
if (!seen.has(key)) {
|
|
206
|
-
entries.push(host);
|
|
207
|
-
seen.add(key);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
process.env.NO_PROXY = entries.join(",");
|
|
260
|
+
mergeNoProxyEntries(configured);
|
|
211
261
|
configureSocks5Fetch();
|
|
212
262
|
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
2
|
|
|
3
3
|
const pendingTopLevelDeletions = new WeakMap<OcxConfig, Set<string>>();
|
|
4
|
+
const pendingObjectChildDeletions = new WeakMap<OcxConfig, Map<string, Set<string>>>();
|
|
4
5
|
export const CONFIG_REBASE_PROVENANCE_KEY = "configRebaseProvenance";
|
|
5
6
|
|
|
7
|
+
export type ConfigObjectChildDeletions = Map<string, Set<string>>;
|
|
8
|
+
|
|
9
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
11
|
+
const prototype = Object.getPrototypeOf(value);
|
|
12
|
+
return prototype === Object.prototype || prototype === null;
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export function parsedConfigRebaseDeletionKeys(config: OcxConfig): Set<string> | null {
|
|
7
16
|
const value = config.configRebaseProvenance;
|
|
8
17
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -63,15 +72,79 @@ export function deleteConfigTopLevelKey<K extends keyof OcxConfig>(config: OcxCo
|
|
|
63
72
|
pendingTopLevelDeletions.set(config, deleted);
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
/** Delete one child from a record-valued field without tombstoning concurrent sibling keys. */
|
|
76
|
+
export function deleteConfigObjectChildKey<K extends keyof OcxConfig>(
|
|
77
|
+
config: OcxConfig,
|
|
78
|
+
key: K,
|
|
79
|
+
childKey: string,
|
|
80
|
+
): void {
|
|
81
|
+
const record = config as unknown as Record<string, unknown>;
|
|
82
|
+
const value = record[key as string];
|
|
83
|
+
if (isPlainRecord(value)) {
|
|
84
|
+
delete value[childKey];
|
|
85
|
+
if (Object.keys(value).length === 0) delete record[key as string];
|
|
86
|
+
}
|
|
87
|
+
const byParent = pendingObjectChildDeletions.get(config) ?? new Map<string, Set<string>>();
|
|
88
|
+
const deleted = byParent.get(key as string) ?? new Set<string>();
|
|
89
|
+
deleted.add(childKey);
|
|
90
|
+
byParent.set(key as string, deleted);
|
|
91
|
+
pendingObjectChildDeletions.set(config, byParent);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Materialize record containers so the normal recursive three-way merge can adopt
|
|
96
|
+
* concurrent sibling keys. Returned child tombstones must be applied after that merge.
|
|
97
|
+
*/
|
|
98
|
+
export function prepareConfigObjectChildDeletionRebase(config: OcxConfig): ConfigObjectChildDeletions {
|
|
99
|
+
const pending = pendingObjectChildDeletions.get(config);
|
|
100
|
+
const active: ConfigObjectChildDeletions = new Map();
|
|
101
|
+
if (!pending) return active;
|
|
102
|
+
const record = config as unknown as Record<string, unknown>;
|
|
103
|
+
for (const [key, children] of pending) {
|
|
104
|
+
const current = record[key];
|
|
105
|
+
const deleted = new Set([...children].filter(child =>
|
|
106
|
+
!isPlainRecord(current) || !Object.hasOwn(current, child) || current[child] === undefined));
|
|
107
|
+
if (deleted.size === 0) continue;
|
|
108
|
+
active.set(key, deleted);
|
|
109
|
+
if (!isPlainRecord(current)) record[key] = {};
|
|
110
|
+
}
|
|
111
|
+
return active;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reassert explicit child deletions after rebasing, then omit an empty parent record. */
|
|
115
|
+
export function applyConfigObjectChildDeletions(
|
|
116
|
+
config: OcxConfig,
|
|
117
|
+
deletions: ConfigObjectChildDeletions,
|
|
118
|
+
): void {
|
|
119
|
+
const record = config as unknown as Record<string, unknown>;
|
|
120
|
+
for (const [key, children] of deletions) {
|
|
121
|
+
const current = record[key];
|
|
122
|
+
if (!isPlainRecord(current)) continue;
|
|
123
|
+
for (const child of children) delete current[child];
|
|
124
|
+
if (Object.keys(current).length === 0) delete record[key];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
66
128
|
export function clearPendingConfigTopLevelDeletions(config: OcxConfig): void {
|
|
67
129
|
pendingTopLevelDeletions.delete(config);
|
|
68
130
|
}
|
|
69
131
|
|
|
132
|
+
export function clearPendingConfigObjectChildDeletions(config: OcxConfig): void {
|
|
133
|
+
pendingObjectChildDeletions.delete(config);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Consume both kinds of pending deletion only after a successful config publication. */
|
|
137
|
+
export function clearPendingConfigDeletions(config: OcxConfig): void {
|
|
138
|
+
clearPendingConfigObjectChildDeletions(config);
|
|
139
|
+
clearPendingConfigTopLevelDeletions(config);
|
|
140
|
+
}
|
|
141
|
+
|
|
70
142
|
/**
|
|
71
143
|
* Capture field replacements and deletion intent for a synchronous live-config save.
|
|
72
144
|
* Restore before yielding on failure: an asynchronous rollback could overwrite a newer
|
|
73
145
|
* mutation. Descriptors preserve absent versus explicitly undefined properties; the
|
|
74
|
-
* private pending
|
|
146
|
+
* private pending deletion collections must also retain their original presence, even when empty.
|
|
147
|
+
* Nested values are not cloned: callers must replace containers before mutating their children.
|
|
75
148
|
* Unrelated fields and the live object's identity/baselines are left in place.
|
|
76
149
|
*/
|
|
77
150
|
export function captureConfigTopLevelRollback(
|
|
@@ -82,6 +155,9 @@ export function captureConfigTopLevelRollback(
|
|
|
82
155
|
.map(key => [key, Object.getOwnPropertyDescriptor(config, key)] as const));
|
|
83
156
|
const pending = pendingTopLevelDeletions.get(config);
|
|
84
157
|
const pendingBefore = pending === undefined ? undefined : new Set(pending);
|
|
158
|
+
const pendingChildren = pendingObjectChildDeletions.get(config);
|
|
159
|
+
const childrenBefore = pendingChildren === undefined ? undefined
|
|
160
|
+
: new Map([...pendingChildren].map(([key, children]) => [key, new Set(children)]));
|
|
85
161
|
return () => {
|
|
86
162
|
for (const [key, descriptor] of descriptors) {
|
|
87
163
|
if (descriptor) Object.defineProperty(config, key, descriptor);
|
|
@@ -90,5 +166,8 @@ export function captureConfigTopLevelRollback(
|
|
|
90
166
|
// The absent fields above are restoration, not new user deletion commands.
|
|
91
167
|
if (pendingBefore === undefined) pendingTopLevelDeletions.delete(config);
|
|
92
168
|
else pendingTopLevelDeletions.set(config, new Set(pendingBefore));
|
|
169
|
+
if (childrenBefore === undefined) pendingObjectChildDeletions.delete(config);
|
|
170
|
+
else pendingObjectChildDeletions.set(config,
|
|
171
|
+
new Map([...childrenBefore].map(([key, children]) => [key, new Set(children)])));
|
|
93
172
|
};
|
|
94
173
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
clientConnectionSchema,
|
|
6
6
|
CODEX_ACCOUNT_PIN_PATTERN,
|
|
7
7
|
codexAccountPrioritiesSchema,
|
|
8
|
+
salvageCodexAccountAutoSwitchThresholds,
|
|
8
9
|
codexPoolSchema,
|
|
9
10
|
codexQuotaAutoRefreshSchema,
|
|
10
11
|
credentialGroupsSchema,
|
|
@@ -217,6 +218,10 @@ export const configSchema = z.object({
|
|
|
217
218
|
// typo cannot trip the backup-and-defaults repair path and wipe providers or
|
|
218
219
|
// pool accounts. Warning emitted in loadConfig.
|
|
219
220
|
codexAccountPriorities: codexAccountPrioritiesSchema.optional().catch(undefined),
|
|
221
|
+
// A bad hand-edited entry must not retire valid overrides on the next unrelated save.
|
|
222
|
+
codexAccountAutoSwitchThresholds: z.unknown().optional().transform(salvageCodexAccountAutoSwitchThresholds),
|
|
223
|
+
// An invalid optional preference must not discard providers or credential rows.
|
|
224
|
+
codexAccountPriorityFailback: z.boolean().optional().catch(false),
|
|
220
225
|
activeCodexAccountPinned: z.string().regex(CODEX_ACCOUNT_PIN_PATTERN).optional().catch(undefined),
|
|
221
226
|
// A malformed hand edit must degrade to false without discarding providers, accounts,
|
|
222
227
|
// or the exact selector map. Live writes remain strict.
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../provider-validation";
|
|
15
15
|
import { isValidCodexAccountNamespaceTarget } from "../../codex/account-namespace-match";
|
|
16
16
|
import { isCodexAccountPriorityKey } from "../../codex/account-priority";
|
|
17
|
+
import { isCodexAccountAutoSwitchThresholdKey, parseCodexAutoSwitchThreshold } from "../../codex/account-auto-switch";
|
|
17
18
|
import { parseAccountPriority } from "../../codex/pool-rotation";
|
|
18
19
|
import { credentialGroupIssues } from "../../routing/identity-domains";
|
|
19
20
|
import { providerDestinationConfigError } from "../../lib/destination-policy";
|
|
@@ -490,7 +491,7 @@ export function modelPreferHostedToolsConfigError(
|
|
|
490
491
|
? (provider.modelAdapters as Record<string, unknown>)[modelId]
|
|
491
492
|
: undefined;
|
|
492
493
|
const resolveEffectiveWire = (modelId: string, currentWire: unknown): unknown => {
|
|
493
|
-
const pinned = pinnedWireAdapter(providerName, modelId);
|
|
494
|
+
const pinned = pinnedWireAdapter(providerName, modelId, provider);
|
|
494
495
|
if (pinned) return pinned;
|
|
495
496
|
const requestedWire = requestedWireFor(modelId);
|
|
496
497
|
if (typeof requestedWire === "string" && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(requestedWire)) {
|
|
@@ -633,6 +634,54 @@ const codexQuotaAutoRefreshEntrySchema = z.object({
|
|
|
633
634
|
const CODEX_QUOTA_AUTO_REFRESH_KEY_ERROR =
|
|
634
635
|
"quota auto-refresh keys must be a Codex pool-account id or the main Codex account and cannot be reserved JavaScript object keys";
|
|
635
636
|
|
|
637
|
+
const CODEX_ACCOUNT_AUTO_SWITCH_THRESHOLDS_RECORD_ERROR =
|
|
638
|
+
"codexAccountAutoSwitchThresholds must be a plain object mapping Codex account ids to usage thresholds";
|
|
639
|
+
const CODEX_ACCOUNT_AUTO_SWITCH_THRESHOLD_KEY_ERROR =
|
|
640
|
+
"usage-threshold keys must be a Codex pool-account id or the main Codex account and cannot be reserved JavaScript object keys";
|
|
641
|
+
const CODEX_ACCOUNT_AUTO_SWITCH_THRESHOLD_VALUE_ERROR =
|
|
642
|
+
"account usage threshold must be an integer between 0 and 100";
|
|
643
|
+
|
|
644
|
+
export const codexAccountAutoSwitchThresholdsSchema = z.custom<Record<string, unknown>>(
|
|
645
|
+
(value): value is Record<string, unknown> => !!value
|
|
646
|
+
&& typeof value === "object"
|
|
647
|
+
&& !Array.isArray(value)
|
|
648
|
+
&& (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null),
|
|
649
|
+
{ error: CODEX_ACCOUNT_AUTO_SWITCH_THRESHOLDS_RECORD_ERROR },
|
|
650
|
+
).superRefine((thresholds, ctx) => {
|
|
651
|
+
for (const [accountId, threshold] of Object.entries(thresholds)) {
|
|
652
|
+
if (!isCodexAccountAutoSwitchThresholdKey(accountId)) {
|
|
653
|
+
ctx.addIssue({
|
|
654
|
+
code: "custom",
|
|
655
|
+
path: [accountId],
|
|
656
|
+
message: CODEX_ACCOUNT_AUTO_SWITCH_THRESHOLD_KEY_ERROR,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
if (parseCodexAutoSwitchThreshold(threshold) === null) {
|
|
660
|
+
ctx.addIssue({
|
|
661
|
+
code: "custom",
|
|
662
|
+
path: [accountId],
|
|
663
|
+
message: CODEX_ACCOUNT_AUTO_SWITCH_THRESHOLD_VALUE_ERROR,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}).pipe(z.record(z.string(), z.number().int()));
|
|
668
|
+
|
|
669
|
+
/** Load only: retain valid overrides from a hand-edited map; writes use the strict schema above. */
|
|
670
|
+
export function salvageCodexAccountAutoSwitchThresholds(value: unknown): Record<string, number> | undefined {
|
|
671
|
+
const parsed = codexAccountAutoSwitchThresholdsSchema.safeParse(value);
|
|
672
|
+
if (parsed.success) return parsed.data;
|
|
673
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
674
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return undefined;
|
|
675
|
+
const valid: Record<string, number> = Object.create(null);
|
|
676
|
+
for (const [accountId, threshold] of Object.entries(value)) {
|
|
677
|
+
const parsedThreshold = parseCodexAutoSwitchThreshold(threshold);
|
|
678
|
+
if (isCodexAccountAutoSwitchThresholdKey(accountId) && parsedThreshold !== null) {
|
|
679
|
+
valid[accountId] = parsedThreshold;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return Object.keys(valid).length ? valid : undefined;
|
|
683
|
+
}
|
|
684
|
+
|
|
636
685
|
export const codexQuotaAutoRefreshSchema = z.custom<Record<string, unknown>>(
|
|
637
686
|
(value): value is Record<string, unknown> => !!value
|
|
638
687
|
&& typeof value === "object"
|
|
@@ -1,24 +1,50 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
-
import { NATIVE_GPT6_ASTRA_MODEL } from "../codex/catalog/native-models";
|
|
2
|
+
import { NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT6_LUNA_MODEL, NATIVE_GPT6_SOL_MODEL } from "../codex/catalog/native-models";
|
|
3
3
|
|
|
4
|
-
export const SUBAGENT_MODELS_VERSION =
|
|
4
|
+
export const SUBAGENT_MODELS_VERSION = 2;
|
|
5
5
|
|
|
6
|
-
/** Native featured defaults
|
|
7
|
-
export const DEFAULT_SUBAGENT_MODELS = [
|
|
8
|
-
NATIVE_GPT6_ASTRA_MODEL, "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5",
|
|
9
|
-
];
|
|
6
|
+
/** Native featured defaults: the GPT-6 trio. Codex advertises at most five picker-visible rows. */
|
|
7
|
+
export const DEFAULT_SUBAGENT_MODELS = [NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT6_SOL_MODEL, NATIVE_GPT6_LUNA_MODEL];
|
|
10
8
|
|
|
11
|
-
/**
|
|
9
|
+
/** Bare native families the version-2 upgrade removes from a roster. */
|
|
10
|
+
const RETIRED_ROSTER_FAMILY = /^gpt-5\.[56](?:-|$)/;
|
|
11
|
+
/** Retired rows whose GPT-6 successor takes their place. */
|
|
12
|
+
const ROSTER_SUCCESSORS: ReadonlyMap<string, string> = new Map([
|
|
13
|
+
["gpt-5.6-sol", NATIVE_GPT6_SOL_MODEL],
|
|
14
|
+
["gpt-5.6-luna", NATIVE_GPT6_LUNA_MODEL],
|
|
15
|
+
]);
|
|
16
|
+
const SUCCESSOR_IDS = new Set(ROSTER_SUCCESSORS.values());
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Replace Sol/Luna with their GPT-6 rows and drop every other bare 5.5/5.6 id, in place order.
|
|
20
|
+
* Ids with a "/" name a routed or account-qualified target and keep their exact spelling.
|
|
21
|
+
* A list that only held retired rows receives the defaults instead of becoming empty.
|
|
22
|
+
*/
|
|
23
|
+
function upgradeRetiredRosterRows(models: readonly string[]): string[] {
|
|
24
|
+
const upgraded: string[] = [];
|
|
25
|
+
for (const model of models) {
|
|
26
|
+
const next = ROSTER_SUCCESSORS.get(model) ?? (RETIRED_ROSTER_FAMILY.test(model) ? null : model);
|
|
27
|
+
if (next === null || (SUCCESSOR_IDS.has(next) && upgraded.includes(next))) continue;
|
|
28
|
+
upgraded.push(next);
|
|
29
|
+
}
|
|
30
|
+
return upgraded.length === 0 && models.length > 0 ? [...DEFAULT_SUBAGENT_MODELS] : upgraded;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One-time upgrades; later user edits (including removing Astra) remain authoritative. */
|
|
12
34
|
export function migrateSubagentModels(config: OcxConfig): boolean {
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
const version = config.subagentModelsVersion ?? 0;
|
|
36
|
+
if (version >= SUBAGENT_MODELS_VERSION) return false;
|
|
37
|
+
if (version < 1) {
|
|
38
|
+
if (config.subagentModels === undefined) {
|
|
39
|
+
config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
|
|
40
|
+
} else {
|
|
41
|
+
const retained = [...new Set([NATIVE_GPT6_ASTRA_MODEL, ...config.subagentModels])].slice(0, 5);
|
|
42
|
+
// Cap first: do not rescue a fifth old choice. Retained 5.5 belongs at the bottom.
|
|
43
|
+
config.subagentModels = retained.filter(model => model !== "gpt-5.5");
|
|
44
|
+
if (retained.includes("gpt-5.5")) config.subagentModels.push("gpt-5.5");
|
|
45
|
+
}
|
|
21
46
|
}
|
|
47
|
+
if (config.subagentModels !== undefined) config.subagentModels = upgradeRetiredRosterRows(config.subagentModels);
|
|
22
48
|
config.subagentModelsVersion = SUBAGENT_MODELS_VERSION;
|
|
23
49
|
return true;
|
|
24
50
|
}
|
package/src/config.ts
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
adoptCustomModelCatalogMigration,
|
|
9
9
|
projectCustomModelCatalogMigration,
|
|
10
10
|
} from "./codex/custom-model-catalog-migration";
|
|
11
|
-
import {
|
|
11
|
+
import { refreshConfigDerivedRegistries } from "./config/derived-registries";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
clearPendingConfigDeletions,
|
|
14
14
|
projectConfigRebaseProvenance,
|
|
15
15
|
} from "./config/rebase-provenance";
|
|
16
16
|
import { getConfigDir, getConfigPath, hardenConfigDir } from "./config/paths";
|
|
@@ -53,6 +53,12 @@ export {
|
|
|
53
53
|
type RuntimePortState,
|
|
54
54
|
} from "./config/process-state";
|
|
55
55
|
export { deleteConfigTopLevelKey } from "./config/rebase-provenance";
|
|
56
|
+
export {
|
|
57
|
+
mutatePersistedConfig,
|
|
58
|
+
setPersistedConfigMutationBeforeCommitForTests,
|
|
59
|
+
type PersistedConfigMutation,
|
|
60
|
+
type PersistedConfigMutationOutcome,
|
|
61
|
+
} from "./config/persisted-mutation";
|
|
56
62
|
export { isValidProviderName, hasOwnProvider } from "./config/provider-name";
|
|
57
63
|
export {
|
|
58
64
|
apiKeyTransportConfigError,
|
|
@@ -146,9 +152,7 @@ import { observeInitialConfigState } from "./config/diagnostics";
|
|
|
146
152
|
import {
|
|
147
153
|
configDiagnosticsFromRaw,
|
|
148
154
|
mergeConfigDefaults,
|
|
149
|
-
readConfigFileSnapshot,
|
|
150
155
|
validateConfigCandidate,
|
|
151
|
-
type ConfigFileSnapshot,
|
|
152
156
|
} from "./config/diagnostics";
|
|
153
157
|
|
|
154
158
|
// replace path — never publishInitialConfigNoReplace
|
|
@@ -247,6 +251,13 @@ export function loadConfig(): OcxConfig {
|
|
|
247
251
|
warnDegradedCredentialGroups(parsed);
|
|
248
252
|
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
249
253
|
}
|
|
254
|
+
// Only object-shaped configs are repairable. Spreading another JSON value
|
|
255
|
+
// into defaults can manufacture a valid config and bypass the invalid-file
|
|
256
|
+
// backup.
|
|
257
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
258
|
+
warnAndBackupInvalidConfig(configPath, result.error);
|
|
259
|
+
return getDefaultConfig();
|
|
260
|
+
}
|
|
250
261
|
// Schema validation failed — merge defaults into the raw object instead of
|
|
251
262
|
// discarding it entirely, so pool accounts and providers survive a missing
|
|
252
263
|
// field like defaultProvider.
|
|
@@ -343,8 +354,8 @@ export function initializePersistedConfigIfMissing(
|
|
|
343
354
|
adoptCustomModelCatalogMigration(config, persisted);
|
|
344
355
|
if (persisted.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
|
|
345
356
|
else config.configRebaseProvenance = structuredClone(persisted.configRebaseProvenance);
|
|
346
|
-
|
|
347
|
-
|
|
357
|
+
clearPendingConfigDeletions(config);
|
|
358
|
+
refreshConfigDerivedRegistries(persisted);
|
|
348
359
|
return "created";
|
|
349
360
|
} catch (cause) {
|
|
350
361
|
if (published) throw new InitialConfigPublicationError("published", false, false, { cause });
|
|
@@ -367,94 +378,6 @@ export function saveConfig(config: OcxConfig): void {
|
|
|
367
378
|
adoptCustomModelCatalogMigration(config, withProvenance);
|
|
368
379
|
if (withProvenance.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
|
|
369
380
|
else config.configRebaseProvenance = structuredClone(withProvenance.configRebaseProvenance);
|
|
370
|
-
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export type PersistedConfigMutation<T> = {
|
|
375
|
-
changed: boolean;
|
|
376
|
-
value: T;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
export type PersistedConfigMutationOutcome<T> =
|
|
380
|
-
| { status: "committed" | "unchanged"; value: T }
|
|
381
|
-
| { status: "unavailable"; reason: "missing" | "invalid" | "conflict" };
|
|
382
|
-
|
|
383
|
-
const CONFIG_MUTATION_MAX_REBASE_ATTEMPTS = 3;
|
|
384
|
-
let persistedConfigMutationBeforeCommitForTests: (() => void) | null = null;
|
|
385
|
-
|
|
386
|
-
/** Test-only one-shot seam: inject a competing mutation after the first decision, before freshness revalidation. */
|
|
387
|
-
export function setPersistedConfigMutationBeforeCommitForTests(hook: (() => void) | null): void {
|
|
388
|
-
persistedConfigMutationBeforeCommitForTests = hook;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function unavailableConfigMutationReason(snapshot: ConfigFileSnapshot): "missing" | "invalid" {
|
|
392
|
-
return snapshot.diagnostics.source === "default" ? "missing" : "invalid";
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Patch a schema-valid on-disk config under the shared mutation lock. Cooperating writers are
|
|
397
|
-
* serialized; the callback is rerun on the newest snapshot so observed direct byte changes rebase
|
|
398
|
-
* and credential predicates are re-evaluated immediately before the atomic commit. A writer that
|
|
399
|
-
* ignores the coordinator can still change bytes after the final check because the filesystem has
|
|
400
|
-
* no portable conditional rename. Missing or malformed config always fails closed and is never
|
|
401
|
-
* recreated from a prior snapshot.
|
|
402
|
-
*/
|
|
403
|
-
export function mutatePersistedConfig<T>(
|
|
404
|
-
mutate: (config: OcxConfig) => PersistedConfigMutation<T>,
|
|
405
|
-
): PersistedConfigMutationOutcome<T> {
|
|
406
|
-
// Avoid creating/opening the coordinator database for a read-path update that already knows
|
|
407
|
-
// there is no valid config. The same check runs again under the transaction for authority.
|
|
408
|
-
const observed = readConfigFileSnapshot();
|
|
409
|
-
if (observed.diagnostics.source !== "file" || observed.raw === undefined) {
|
|
410
|
-
return { status: "unavailable", reason: unavailableConfigMutationReason(observed) };
|
|
411
|
-
}
|
|
412
|
-
return withConfigMutationLockSync(() => {
|
|
413
|
-
let base = readConfigFileSnapshot();
|
|
414
|
-
for (let attempt = 0; attempt < CONFIG_MUTATION_MAX_REBASE_ATTEMPTS; attempt += 1) {
|
|
415
|
-
if (base.diagnostics.source !== "file" || base.raw === undefined) {
|
|
416
|
-
return { status: "unavailable", reason: unavailableConfigMutationReason(base) };
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const tentativeConfig = structuredClone(base.diagnostics.config);
|
|
420
|
-
const tentative = mutate(tentativeConfig);
|
|
421
|
-
if (!tentative.changed) return { status: "unchanged", value: tentative.value };
|
|
422
|
-
|
|
423
|
-
const hook = persistedConfigMutationBeforeCommitForTests;
|
|
424
|
-
persistedConfigMutationBeforeCommitForTests = null;
|
|
425
|
-
hook?.();
|
|
426
|
-
|
|
427
|
-
const latest = readConfigFileSnapshot();
|
|
428
|
-
if (latest.diagnostics.source !== "file" || latest.raw === undefined) {
|
|
429
|
-
return { status: "unavailable", reason: unavailableConfigMutationReason(latest) };
|
|
430
|
-
}
|
|
431
|
-
if (latest.raw !== base.raw) {
|
|
432
|
-
base = latest;
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Re-run against a fresh clone even when config bytes are unchanged: a Codex credential
|
|
437
|
-
// generation lives in a separate file and may have changed at the injected seam.
|
|
438
|
-
const confirmedConfig = structuredClone(latest.diagnostics.config);
|
|
439
|
-
const confirmed = mutate(confirmedConfig);
|
|
440
|
-
if (!confirmed.changed) return { status: "unchanged", value: confirmed.value };
|
|
441
|
-
|
|
442
|
-
const commitBase = readConfigFileSnapshot();
|
|
443
|
-
if (commitBase.diagnostics.source !== "file" || commitBase.raw === undefined) {
|
|
444
|
-
return { status: "unavailable", reason: unavailableConfigMutationReason(commitBase) };
|
|
445
|
-
}
|
|
446
|
-
if (commitBase.raw !== latest.raw) {
|
|
447
|
-
base = commitBase;
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const projected = projectCustomModelCatalogMigration(
|
|
452
|
-
commitBase.diagnostics.config,
|
|
453
|
-
projectConfigRebaseProvenance(confirmedConfig),
|
|
454
|
-
);
|
|
455
|
-
if (persistConfigUnlocked(projected)) bumpGenerationForCooperatingConfigWrite();
|
|
456
|
-
return { status: "committed", value: confirmed.value };
|
|
457
|
-
}
|
|
458
|
-
return { status: "unavailable", reason: "conflict" };
|
|
381
|
+
clearPendingConfigDeletions(config);
|
|
459
382
|
});
|
|
460
383
|
}
|