@bitkyc08/opencodex 2.86.0 → 2.87.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/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isApprovedCodexProviderUrl } from "../provider-routing";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import {
|
|
3
4
|
hasInjectedCodexRouting,
|
|
@@ -54,7 +55,7 @@ export function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/** Classify actual routing dependency separately from opencodex ownership. */
|
|
57
|
-
export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
58
|
+
export function classifyCodexRouting(content: string, approvedProviderUrl?: (id: string, url: string) => boolean): CodexRoutingKind {
|
|
58
59
|
const rootBaseUrl = rootTomlString(content, "openai_base_url");
|
|
59
60
|
if (rootBaseUrl) {
|
|
60
61
|
const endpoint = classifyRoutingEndpoint(rootBaseUrl);
|
|
@@ -74,7 +75,7 @@ export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
|
74
75
|
if (providerBaseUrl) {
|
|
75
76
|
const endpoint = classifyRoutingEndpoint(providerBaseUrl);
|
|
76
77
|
if (endpoint === "unknown") return "unknown";
|
|
77
|
-
if (rootProvider === "opencodex") return "opencodex-local";
|
|
78
|
+
if (rootProvider === "opencodex" || endpoint === "local" && approvedProviderUrl?.(rootProvider, providerBaseUrl)) return "opencodex-local";
|
|
78
79
|
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
79
80
|
}
|
|
80
81
|
if (
|
|
@@ -102,7 +103,7 @@ export function getCodexRoutingKind(): CodexRoutingKind {
|
|
|
102
103
|
const path = CODEX_CONFIG_PATH;
|
|
103
104
|
if (!existsSync(path)) return "native";
|
|
104
105
|
try {
|
|
105
|
-
return classifyCodexRouting(readFileSync(path, "utf8"));
|
|
106
|
+
return classifyCodexRouting(readFileSync(path, "utf8"), isApprovedCodexProviderUrl);
|
|
106
107
|
} catch {
|
|
107
108
|
return "unknown";
|
|
108
109
|
}
|
package/src/codex/inject.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { injectApprovedCodexProvider } from "./provider-routing";
|
|
1
2
|
import { beginCodexWriteSection, cleanExternalProviderJournal, publishCodexArtifact } from "./inject/config-write-section";
|
|
2
3
|
import { closeSync, existsSync, openSync, readFileSync, statSync } from "node:fs";
|
|
3
4
|
import { dirname } from "node:path";
|
|
@@ -46,8 +47,8 @@ import {
|
|
|
46
47
|
writeJournal,
|
|
47
48
|
} from "./journal";
|
|
48
49
|
import { HISTORY_RELABEL_STANDS_DOWN } from "./history-provider";
|
|
50
|
+
import { formatApplyHistoryFailure } from "./inject/history-failure";
|
|
49
51
|
import {
|
|
50
|
-
describeHistoryJobFailure,
|
|
51
52
|
deriveCodexHistoryOperation,
|
|
52
53
|
resolveCodexHistoryJobTarget,
|
|
53
54
|
runCodexHistoryJob,
|
|
@@ -142,6 +143,8 @@ export interface CodexInjectResult {
|
|
|
142
143
|
message: string;
|
|
143
144
|
/** False when injection intentionally preserves configuration owned by another provider. */
|
|
144
145
|
configApplied?: false;
|
|
146
|
+
/** True only after a successful configuration transaction changes config.toml bytes. */
|
|
147
|
+
configChanged?: boolean;
|
|
145
148
|
/**
|
|
146
149
|
* Structured read-only history preflight refusal; never parsed from display text.
|
|
147
150
|
*
|
|
@@ -240,6 +243,10 @@ async function injectCodexConfigImpl(
|
|
|
240
243
|
const rawContent = missingConfig ? "" : readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
241
244
|
const activeProvider = externalCodexModelProvider(rawContent);
|
|
242
245
|
if (activeProvider) {
|
|
246
|
+
const approved = injectApprovedCodexProvider(activeProvider, routingTarget, options.beforeClientWrite ? () => runClientWriteGuard(options.beforeClientWrite) : undefined, options.validateOnly);
|
|
247
|
+
if (approved) {
|
|
248
|
+
return { success: true, configChanged: approved.configChanged, ...(options.validateOnly ? { configApplied: false as const } : {}), message: `Approved Codex provider ${tomlString(activeProvider)} routes through OpenCodex; provider identity and transport settings preserved.` };
|
|
249
|
+
}
|
|
243
250
|
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
244
251
|
// replay that stale snapshot over externally managed config.
|
|
245
252
|
if (!options.validateOnly) {
|
|
@@ -258,7 +265,7 @@ async function injectCodexConfigImpl(
|
|
|
258
265
|
: {}),
|
|
259
266
|
message:
|
|
260
267
|
`⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
261
|
-
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
268
|
+
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n To authorize URL-only management, run ocx codex-provider allow ${tomlString(activeProvider)} and confirm.\n` +
|
|
262
269
|
` Configure that provider for Responses passthrough at ${routingTarget.baseUrl}` +
|
|
263
270
|
`${routingTarget.requiresAdmissionToken ? ` with x-opencodex-api-key from ${routingTarget.tokenEnv}` : ""}.\n` +
|
|
264
271
|
` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
|
|
@@ -717,6 +724,11 @@ async function injectCodexConfigImpl(
|
|
|
717
724
|
releaseConfigWriteLock(configLock.handle);
|
|
718
725
|
heldConfigWriteLock = undefined;
|
|
719
726
|
}
|
|
727
|
+
// Both write paths have committed, including the coordinator's post-callback checks.
|
|
728
|
+
// Compare against the admitted original, not post-v1-reconcile input: the native
|
|
729
|
+
// feature transition is part of this transaction too. Refusals/rollbacks return
|
|
730
|
+
// above and must never publish this receipt. No filesystem reread after unlock.
|
|
731
|
+
const configChanged = Boolean(missingConfig) || effectivePlan.content !== rawContent;
|
|
720
732
|
// Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
|
|
721
733
|
// the opposite: a one-time migration of previously re-tagged threads BACK to openai (restore
|
|
722
734
|
// machinery; cheap no-op when there is nothing to migrate). The client-compaction opt-in keeps
|
|
@@ -799,6 +811,7 @@ async function injectCodexConfigImpl(
|
|
|
799
811
|
if (effectivePlan.keptUserBaseUrl && effectivePlan.keepRootOverrideAlongsideTable) {
|
|
800
812
|
return {
|
|
801
813
|
success: true,
|
|
814
|
+
configChanged,
|
|
802
815
|
...(effectivePlan.nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning } : {}),
|
|
803
816
|
...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
|
|
804
817
|
message:
|
|
@@ -817,6 +830,7 @@ async function injectCodexConfigImpl(
|
|
|
817
830
|
if (effectivePlan.keptUserBaseUrl) {
|
|
818
831
|
return {
|
|
819
832
|
success: true,
|
|
833
|
+
configChanged,
|
|
820
834
|
...(effectivePlan.nativeSubagentDefaultsWarning
|
|
821
835
|
? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning }
|
|
822
836
|
: {}),
|
|
@@ -839,6 +853,7 @@ async function injectCodexConfigImpl(
|
|
|
839
853
|
: `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url + realtime sideband override).\n`;
|
|
840
854
|
return {
|
|
841
855
|
success: true,
|
|
856
|
+
configChanged,
|
|
842
857
|
...(effectivePlan.nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning } : {}),
|
|
843
858
|
...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
|
|
844
859
|
message:
|
|
@@ -860,31 +875,7 @@ export function getCodexConfigPath(): string {
|
|
|
860
875
|
return CODEX_CONFIG_PATH;
|
|
861
876
|
}
|
|
862
877
|
|
|
863
|
-
|
|
864
|
-
* Frame one failed apply history job honestly.
|
|
865
|
-
*
|
|
866
|
-
* A genuine lock keeps the established deferred/SKIPPED wording; any other
|
|
867
|
-
* reason names itself instead of blaming the Codex app/IDE.
|
|
868
|
-
*/
|
|
869
|
-
export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legacyMode: boolean): string {
|
|
870
|
-
// A busy database is a deferral no matter which half observed it: the lock
|
|
871
|
-
// contended (blocked/busy), or the worker acquired the lock and then found
|
|
872
|
-
// SQLite busy (failed with a busy history reason). Only those keep the
|
|
873
|
-
// deferred headline; every other failure is a real "NOT changed".
|
|
874
|
-
const busy =
|
|
875
|
-
(outcome.kind === "blocked" && outcome.reason === "busy") ||
|
|
876
|
-
(outcome.kind === "failed" && outcome.historyFailureReason === "busy");
|
|
877
|
-
const partiallyChanged = outcome.kind === "failed"
|
|
878
|
-
&& ((outcome.rows ?? 0) > 0 || (outcome.files ?? 0) > 0);
|
|
879
|
-
const headline = partiallyChanged
|
|
880
|
-
? "Codex resume history changed but did not converge"
|
|
881
|
-
: legacyMode
|
|
882
|
-
? "Codex resume history sync SKIPPED"
|
|
883
|
-
: busy
|
|
884
|
-
? "Codex resume history metadata restore deferred"
|
|
885
|
-
: "Codex resume history NOT changed";
|
|
886
|
-
return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
|
|
887
|
-
}
|
|
878
|
+
export { formatApplyHistoryFailure };
|
|
888
879
|
|
|
889
880
|
export {
|
|
890
881
|
providerBaseHost,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
3
|
+
import { codexAccountUsesRemainingQuota } from "./account-use-remaining";
|
|
4
|
+
import { isDeclaredReasoningEffort } from "../reasoning-effort";
|
|
2
5
|
import { getMainPolicyQuota } from "./quota";
|
|
3
6
|
import { MAIN_ACCOUNT_HARD_LOCK_PERCENT, MAIN_ACCOUNT_HARD_LOCK_SHORT_PERCENT, MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "./quota-types";
|
|
4
7
|
|
|
@@ -13,9 +16,35 @@ export interface MainAccountHardLockStatus {
|
|
|
13
16
|
window?: "short" | "long";
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
type PolicyConfig = Pick<OcxConfig, "codexMainAccountHardLock" | "codexMainAccountHardLockThresholds">;
|
|
19
|
+
type PolicyConfig = Pick<OcxConfig, "codexMainAccountHardLock" | "codexMainAccountHardLockThresholds" | "codexMainAccountHardLockFallback" | "codexUseRemainingQuotaAccountIds">;
|
|
20
|
+
|
|
21
|
+
/** The reroute destination for requests the lock blocks, when one is configured and readable. */
|
|
22
|
+
export interface MainAccountHardLockFallback {
|
|
23
|
+
model: string;
|
|
24
|
+
reasoningEffort?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The configured fallback, or `undefined` when nothing qualifies. The dashboard row keeps
|
|
29
|
+
* the configured model while its switch is off, so `enabled: false` pauses the reroute
|
|
30
|
+
* without forgetting the destination. A malformed hand edit — blank or padded model,
|
|
31
|
+
* undeclared effort — disables the reroute entirely rather than degrading into a
|
|
32
|
+
* half-applied one: the request path must never guess which part of a broken setting to
|
|
33
|
+
* keep. This is the same disable-don't-widen reading the compaction override takes, and
|
|
34
|
+
* the load/write boundaries report the malformation separately.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveMainAccountHardLockFallback(config: PolicyConfig | undefined): MainAccountHardLockFallback | undefined {
|
|
37
|
+
const raw = config?.codexMainAccountHardLockFallback;
|
|
38
|
+
if (!raw || raw.enabled === false) return undefined;
|
|
39
|
+
const model = typeof raw.model === "string" ? raw.model : "";
|
|
40
|
+
if (model.length === 0 || model !== model.trim() || /\s/.test(model)) return undefined;
|
|
41
|
+
if (raw.reasoningEffort !== undefined
|
|
42
|
+
&& (typeof raw.reasoningEffort !== "string" || !isDeclaredReasoningEffort(raw.reasoningEffort))) return undefined;
|
|
43
|
+
return raw.reasoningEffort !== undefined ? { model, reasoningEffort: raw.reasoningEffort } : { model };
|
|
44
|
+
}
|
|
17
45
|
|
|
18
46
|
export function resolveMainAccountHardLockThresholds(config: PolicyConfig | undefined): { short: number; long: number } {
|
|
47
|
+
if (codexAccountUsesRemainingQuota(config, MAIN_CODEX_ACCOUNT_ID)) return { short: 100, long: 100 };
|
|
19
48
|
const valid = (value: unknown, fallback: number): number => typeof value === "number"
|
|
20
49
|
&& Number.isInteger(value) && value >= MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT && value <= 100 ? value : fallback;
|
|
21
50
|
const long = valid(config?.codexMainAccountHardLockThresholds?.long, MAIN_ACCOUNT_HARD_LOCK_PERCENT);
|
|
@@ -3,7 +3,7 @@ import { loadConfig } from "../config";
|
|
|
3
3
|
import { initializeMainAccountPolicyBinding } from "./account-lifecycle";
|
|
4
4
|
import { isMainAccountHardLockEnabled } from "./main-account-hard-lock";
|
|
5
5
|
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
6
|
-
import { MAIN_CODEX_ACCOUNT_ID } from "./
|
|
6
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
7
7
|
import {
|
|
8
8
|
probeNativeProfileRecoveryState,
|
|
9
9
|
resolveNativeProfileContext,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isCompactionItemType,
|
|
27
27
|
SUMMARY_PREFIX,
|
|
28
28
|
} from "../responses/compaction";
|
|
29
|
+
import { decodeRetainedCompaction } from "../responses/retained-compaction";
|
|
29
30
|
import { resolveCodexHomeDir } from "./home";
|
|
30
31
|
import { resolveCodexStateDbPath } from "./paths";
|
|
31
32
|
|
|
@@ -96,21 +97,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
96
97
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
function lowerCompactionItem(item: unknown): {
|
|
100
|
+
function lowerCompactionItem(item: unknown): { items: unknown[]; changed: boolean } {
|
|
100
101
|
if (!isRecord(item) || !isCompactionItemType(item.type)) {
|
|
101
|
-
return { item, changed: false };
|
|
102
|
+
return { items: [item], changed: false };
|
|
102
103
|
}
|
|
103
104
|
if (typeof item.encrypted_content !== "string") {
|
|
104
|
-
return { item, changed: false };
|
|
105
|
+
return { items: [item], changed: false };
|
|
105
106
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
107
|
+
const retained = decodeRetainedCompaction(item.encrypted_content);
|
|
108
|
+
const summary = retained?.summary ?? decodeCompactionSummary(item.encrypted_content);
|
|
109
|
+
if (summary === null) return { items: [item], changed: false };
|
|
108
110
|
return {
|
|
109
|
-
|
|
111
|
+
items: [{
|
|
110
112
|
type: "message",
|
|
111
113
|
role: "user",
|
|
112
114
|
content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n${summary}` }],
|
|
113
|
-
},
|
|
115
|
+
}, ...(retained?.reasoning ?? []).map(text => ({ type: "message", role: "user", content: [{ type: "input_text", text }] }))],
|
|
114
116
|
changed: true,
|
|
115
117
|
};
|
|
116
118
|
}
|
|
@@ -129,10 +131,10 @@ function rewriteJsonlLine(line: string): { line: string; replaced: number } {
|
|
|
129
131
|
if (!Array.isArray(history)) return { line, replaced: 0 };
|
|
130
132
|
|
|
131
133
|
let replaced = 0;
|
|
132
|
-
const replacementHistory = history.
|
|
134
|
+
const replacementHistory = history.flatMap(item => {
|
|
133
135
|
const lowered = lowerCompactionItem(item);
|
|
134
136
|
if (lowered.changed) replaced += 1;
|
|
135
|
-
return lowered.
|
|
137
|
+
return lowered.items;
|
|
136
138
|
});
|
|
137
139
|
if (replaced === 0) return { line, replaced: 0 };
|
|
138
140
|
|
|
@@ -146,7 +148,7 @@ function rewriteJsonlLine(line: string): { line: string; replaced: number } {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
/**
|
|
149
|
-
* Convert OpenCodeX-owned `ocx1:` compaction items into ordinary replayable user messages.
|
|
151
|
+
* Convert OpenCodeX-owned `ocx1:`/`ocx2:` compaction items into ordinary replayable user messages.
|
|
150
152
|
*
|
|
151
153
|
* Only the authoritative `compacted.payload.replacement_history` snapshot is changed. Earlier
|
|
152
154
|
* response-item events are historical output and are deliberately preserved byte-for-byte.
|
package/src/codex/paths.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { expandUserPath } from "../config";
|
|
4
4
|
import { defaultCodexHome } from "./home";
|
|
@@ -144,3 +144,88 @@ export function readRootTomlString(content: string, key: string): string | null
|
|
|
144
144
|
export function resolveCodexConfigPath(path: string): string {
|
|
145
145
|
return isAbsolute(path) ? path : join(CODEX_HOME, path);
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Active Codex home resolution.
|
|
150
|
+
//
|
|
151
|
+
// These resolve `CODEX_HOME` at call time rather than at import time, which is
|
|
152
|
+
// what lets a test or a sibling-home probe point fixture state somewhere else
|
|
153
|
+
// after this module has already loaded. They live here, next to the constants
|
|
154
|
+
// they fall back to, so a caller that only needs a path never has to reach the
|
|
155
|
+
// catalog parser.
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
export function samePath(a: string, b: string): boolean {
|
|
159
|
+
const left = resolve(a);
|
|
160
|
+
const right = resolve(b);
|
|
161
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function activeCodexHome(): string | null {
|
|
165
|
+
const raw = process.env.CODEX_HOME?.trim();
|
|
166
|
+
if (!raw) return null;
|
|
167
|
+
const path = resolve(expandUserPath(raw));
|
|
168
|
+
try {
|
|
169
|
+
return realpathSync.native(path);
|
|
170
|
+
} catch {
|
|
171
|
+
return path;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function activeCodexConfigPath(): string {
|
|
176
|
+
const home = activeCodexHome();
|
|
177
|
+
return home ? join(home, "config.toml") : CODEX_CONFIG_PATH;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function activeDefaultCatalogPath(): string {
|
|
181
|
+
const home = activeCodexHome();
|
|
182
|
+
return home ? join(home, "opencodex-catalog.json") : DEFAULT_CATALOG_PATH;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function activeCodexModelsCachePath(): string {
|
|
186
|
+
const home = activeCodexHome();
|
|
187
|
+
return home ? join(home, "models_cache.json") : CODEX_MODELS_CACHE_PATH;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function resolveActiveCodexConfigPath(path: string): string {
|
|
191
|
+
const home = activeCodexHome();
|
|
192
|
+
return home ? resolve(home, path) : resolveCodexConfigPath(path);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function isDefaultCatalogPath(path: string): boolean {
|
|
196
|
+
return samePath(path, activeDefaultCatalogPath());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function readCodexCatalogPath(): string {
|
|
200
|
+
const home = activeCodexHome();
|
|
201
|
+
if (home) return readCodexCatalogPathForHome(home);
|
|
202
|
+
try {
|
|
203
|
+
const configPath = activeCodexConfigPath();
|
|
204
|
+
if (existsSync(configPath)) {
|
|
205
|
+
const toml = readFileSync(configPath, "utf-8");
|
|
206
|
+
const path = readRootTomlString(toml, "model_catalog_json");
|
|
207
|
+
if (path) return resolveActiveCodexConfigPath(path);
|
|
208
|
+
}
|
|
209
|
+
} catch { /* ignore */ }
|
|
210
|
+
return activeDefaultCatalogPath();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resolve the configured catalog without consulting ambient CODEX_HOME again.
|
|
215
|
+
*
|
|
216
|
+
* `configText` is for a caller that has already read that same `config.toml` under
|
|
217
|
+
* its own constraints - the prompt-text probe reads it bounded, on the request
|
|
218
|
+
* thread - so resolving the catalog does not cost a second, unbounded read of the
|
|
219
|
+
* file the caller is holding. Omitting it keeps the original behaviour.
|
|
220
|
+
*/
|
|
221
|
+
export function readCodexCatalogPathForHome(codexHome: string, configText?: string): string {
|
|
222
|
+
try {
|
|
223
|
+
const configPath = join(codexHome, "config.toml");
|
|
224
|
+
if (configText !== undefined || existsSync(configPath)) {
|
|
225
|
+
const toml = configText ?? readFileSync(configPath, "utf-8");
|
|
226
|
+
const path = readRootTomlString(toml, "model_catalog_json");
|
|
227
|
+
if (path) return resolve(codexHome, path);
|
|
228
|
+
}
|
|
229
|
+
} catch { /* ignore */ }
|
|
230
|
+
return join(codexHome, "opencodex-catalog.json");
|
|
231
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadConfig } from "../config";
|
|
6
|
+
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
7
|
+
import { atomicWriteFile } from "../config/atomic-write";
|
|
8
|
+
import { withConfigMutationLockSync } from "../config/mutation-lock";
|
|
9
|
+
import { getConfigDir } from "../config/paths";
|
|
10
|
+
import { assertCodexHomeOwner } from "./codex-home-owner";
|
|
11
|
+
import { ConfigWriteLockRefusal, publishConfigWriteTarget, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "./config-write-lock";
|
|
12
|
+
import { providerTableStart } from "./injected-marker";
|
|
13
|
+
import { CODEX_CONFIG_PATH, getCodexHome, tomlString } from "./paths";
|
|
14
|
+
import { rootSourceLines, sourceAssignment, sourceText } from "./toml-source-lines";
|
|
15
|
+
import { JOURNAL_PATH, removeJournal } from "./journal";
|
|
16
|
+
import { externalCodexModelProvider } from "./inject/config-toml";
|
|
17
|
+
import { readBoundedCodexConfig } from "./inject/bounded-config-reader";
|
|
18
|
+
import { isLoopbackHostname } from "./loopback-target";
|
|
19
|
+
import { validateCodexRoutingTarget, type CodexRoutingTarget } from "./inject/routing-target";
|
|
20
|
+
|
|
21
|
+
const approval = z.object({ original: z.string(), originalUrl: z.string().url(), managedUrls: z.array(z.string().url()).max(2) }).strict();
|
|
22
|
+
const recordSchema = z.object({ version: z.literal(1), codexHome: z.string(), providers: z.record(z.string(), approval) }).strict();
|
|
23
|
+
type RoutingRecord = z.infer<typeof recordSchema>;
|
|
24
|
+
type Approval = z.infer<typeof approval>;
|
|
25
|
+
let beforePublishForTests: (() => void) | undefined;
|
|
26
|
+
export function setBeforeProviderRoutingPublishForTests(hook: typeof beforePublishForTests): void { beforePublishForTests = hook; }
|
|
27
|
+
|
|
28
|
+
function recordPath(): string {
|
|
29
|
+
const homeId = createHash("sha256").update(getCodexHome()).digest("hex");
|
|
30
|
+
return join(getConfigDir(), `codex-provider-routing-${homeId}.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readRecord(): RoutingRecord {
|
|
34
|
+
const content = readBoundedCodexConfig(recordPath());
|
|
35
|
+
if (content === null) return { version: 1, codexHome: getCodexHome(), providers: {} };
|
|
36
|
+
const record = recordSchema.parse(JSON.parse(content));
|
|
37
|
+
if (record.codexHome !== getCodexHome()) throw new Error("Codex provider routing consent belongs to another home.");
|
|
38
|
+
for (const saved of Object.values(record.providers)) {
|
|
39
|
+
const parsed = Bun.TOML.parse(saved.original) as Record<string, unknown>;
|
|
40
|
+
if (Object.keys(parsed).length !== 1 || parsed.base_url !== saved.originalUrl
|
|
41
|
+
|| saved.managedUrls.some(url => !isLoopbackHostname(new URL(url).hostname))) {
|
|
42
|
+
throw new Error("Codex provider routing recovery evidence is invalid; existing files were preserved.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return record;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Only regular Responses provider tables with no separate credential transport are eligible. */
|
|
49
|
+
function providerUrl(content: string, id: string, checkCredentials = true) {
|
|
50
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(id) || ["__proto__", "constructor", "prototype", "openai", "opencodex", "ollama", "lmstudio"].includes(id)) {
|
|
51
|
+
throw new Error("Select a custom provider ID using letters, digits, hyphens or underscores.");
|
|
52
|
+
}
|
|
53
|
+
const parsed = Bun.TOML.parse(content) as Record<string, unknown>;
|
|
54
|
+
const table = (parsed.model_providers as Record<string, Record<string, unknown>> | undefined)?.[id];
|
|
55
|
+
if (!table || checkCredentials && ((table.wire_api !== undefined && table.wire_api !== "responses")
|
|
56
|
+
|| table.requires_openai_auth !== true
|
|
57
|
+
|| ["env_key", "experimental_bearer_token", "auth", "http_headers", "env_http_headers"].some(key => Object.hasOwn(table, key)))) {
|
|
58
|
+
throw new Error("Routing consent requires a Responses provider using OpenAI authentication without custom credentials or headers.");
|
|
59
|
+
}
|
|
60
|
+
const { bom, lines } = rootSourceLines(content);
|
|
61
|
+
const start = providerTableStart(lines.map(line => line.structural ? line.text : ""), id);
|
|
62
|
+
if (start < 0) throw new Error("Routing consent requires an explicit provider table.");
|
|
63
|
+
let end = start + 1;
|
|
64
|
+
while (end < lines.length && !(lines[end]!.structural && /^\s*\[/.test(lines[end]!.text))) end++;
|
|
65
|
+
for (let i = start + 1; i < end; i++) {
|
|
66
|
+
const field = sourceAssignment(lines, i, end);
|
|
67
|
+
if (field?.key !== "base_url") continue;
|
|
68
|
+
if (typeof field.value !== "string") break;
|
|
69
|
+
const url = new URL(field.value);
|
|
70
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) break;
|
|
71
|
+
return { url: field.value, assignment: field.text, replace: (text: string) =>
|
|
72
|
+
bom + sourceText(lines.slice(0, field.start)) + text + lines[field.end - 1]!.eol + sourceText(lines.slice(field.end)) };
|
|
73
|
+
}
|
|
74
|
+
throw new Error("Routing consent requires a credential-free HTTP(S) base_url assignment.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inWriteSection<T>(run: (held: LockHandle, content: string, record: RoutingRecord) => T): T {
|
|
78
|
+
assertCodexHomeOwner(getCodexHome());
|
|
79
|
+
const result = withConfigWriteLockHeld(CODEX_CONFIG_PATH, undefined, held => {
|
|
80
|
+
watchConfigWriteTargets(held, [CODEX_CONFIG_PATH, recordPath(), JOURNAL_PATH]);
|
|
81
|
+
const content = readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
82
|
+
return run(held, content, readRecord());
|
|
83
|
+
});
|
|
84
|
+
if (!result.ok) throw new ConfigWriteLockRefusal(result);
|
|
85
|
+
return result.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function publish(path: string, content: string, held: LockHandle): void {
|
|
89
|
+
publishConfigWriteTarget(CODEX_CONFIG_PATH, held, path, (destination, hooks) => atomicWriteFile(destination, content, undefined, hooks));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function save(record: RoutingRecord, held: LockHandle): void {
|
|
93
|
+
publish(recordPath(), JSON.stringify(record, null, 2) + "\n", held);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Read-only ownership evidence: consent alone never claims a URL we did not publish. */
|
|
97
|
+
export function isApprovedCodexProviderUrl(id: string, url: string): boolean {
|
|
98
|
+
const record = readRecord();
|
|
99
|
+
return Object.hasOwn(record.providers, id) && record.providers[id]!.managedUrls.includes(url);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function approvedCodexProviders(): string[] {
|
|
103
|
+
return Object.keys(readRecord().providers);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Capture the exact source before asking; commit refuses a changed provider after the prompt. */
|
|
107
|
+
export function previewCodexProviderConsent(id: string): string {
|
|
108
|
+
return providerUrl(readFileSync(CODEX_CONFIG_PATH, "utf8"), id).assignment;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function allowCodexProviderRouting(id: string, expected: string): void {
|
|
112
|
+
inWriteSection((held, content, record) => {
|
|
113
|
+
const field = providerUrl(content, id);
|
|
114
|
+
if (field.assignment !== expected) throw new Error("Provider changed after confirmation; review it and retry.");
|
|
115
|
+
if (Object.hasOwn(record.providers, id)) throw new Error("Provider is already approved; revoke it before granting new consent.");
|
|
116
|
+
record.providers[id] = { original: field.assignment, originalUrl: field.url, managedUrls: [] };
|
|
117
|
+
save(record, held);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function accepts(fieldUrl: string, saved: Approval): boolean {
|
|
122
|
+
return fieldUrl === saved.originalUrl || saved.managedUrls.includes(fieldUrl);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Called by start/sync, never prompts and never broadens the approved provider set. */
|
|
126
|
+
export function injectApprovedCodexProvider(id: string, target: CodexRoutingTarget, beforeWrite?: () => void, validateOnly = false): false | { configChanged: boolean } {
|
|
127
|
+
return inWriteSection((held, content, record) => {
|
|
128
|
+
if (!Object.hasOwn(record.providers, id)) return false;
|
|
129
|
+
const apply = () => {
|
|
130
|
+
target = validateCodexRoutingTarget(target);
|
|
131
|
+
if (target.requiresAdmissionToken || target.desktopAuthless || target.clientCompaction || !isLoopbackHostname(new URL(target.baseUrl).hostname)) {
|
|
132
|
+
throw new Error("Approved custom providers support only authenticated Codex on a local loopback proxy without client compaction.");
|
|
133
|
+
}
|
|
134
|
+
if (externalCodexModelProvider(content) !== id) throw new Error("Active provider changed; routing was not applied.");
|
|
135
|
+
const field = providerUrl(content, id), saved = record.providers[id]!;
|
|
136
|
+
if (!accepts(field.url, saved)) throw new Error("Approved provider base_url changed outside OpenCodex; revoke and approve it again.");
|
|
137
|
+
if (validateOnly) return { configChanged: false };
|
|
138
|
+
beforeWrite?.();
|
|
139
|
+
if (!shouldSyncCodexOnStart(loadConfig())) return false;
|
|
140
|
+
if (readFileSync(CODEX_CONFIG_PATH, "utf8") !== content) throw new Error("Codex config changed before publication; retry sync.");
|
|
141
|
+
publishConfigWriteTarget(CODEX_CONFIG_PATH, held, JOURNAL_PATH, (_destination, hooks) => removeJournal(hooks));
|
|
142
|
+
if (field.url === target.baseUrl) return { configChanged: false };
|
|
143
|
+
// Write recovery evidence first. Both the previous and candidate URL remain restorable
|
|
144
|
+
// if publication fails or the process exits between these two atomic renames.
|
|
145
|
+
saved.managedUrls = [...new Set([field.url, target.baseUrl])].filter(url => url !== saved.originalUrl);
|
|
146
|
+
save(record, held);
|
|
147
|
+
beforePublishForTests?.();
|
|
148
|
+
publish(CODEX_CONFIG_PATH, field.replace(`base_url = ${tomlString(target.baseUrl)}`), held);
|
|
149
|
+
return { configChanged: true };
|
|
150
|
+
};
|
|
151
|
+
return validateOnly ? apply() : withConfigMutationLockSync(apply);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Restore only our URLs; unrelated edits and provider IDs/history are never rewritten. */
|
|
156
|
+
export function restoreApprovedCodexProviders(revokeId?: string): boolean {
|
|
157
|
+
const ids = approvedCodexProviders();
|
|
158
|
+
if (revokeId !== undefined && !ids.includes(revokeId)) throw new Error("Provider has no routing consent to revoke.");
|
|
159
|
+
if (!ids.length) return false;
|
|
160
|
+
return inWriteSection((held, content, record) => {
|
|
161
|
+
let next = content;
|
|
162
|
+
for (const id of revokeId === undefined ? Object.keys(record.providers) : [revokeId]) {
|
|
163
|
+
const saved = record.providers[id]!;
|
|
164
|
+
const parsed = Bun.TOML.parse(next) as Record<string, unknown>;
|
|
165
|
+
const table = (parsed.model_providers as Record<string, Record<string, unknown>> | undefined)?.[id];
|
|
166
|
+
if (!table || !saved.managedUrls.includes(table.base_url as string)) continue;
|
|
167
|
+
const field = providerUrl(next, id, false);
|
|
168
|
+
if (saved.managedUrls.includes(field.url)) next = field.replace(saved.original);
|
|
169
|
+
}
|
|
170
|
+
if (next !== content) publish(CODEX_CONFIG_PATH, next, held);
|
|
171
|
+
if (revokeId !== undefined) { delete record.providers[revokeId]; save(record, held); }
|
|
172
|
+
return next !== content;
|
|
173
|
+
});
|
|
174
|
+
}
|
package/src/codex/refresh.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { invalidateCodexModelsCache, syncCatalogModels } from "./catalog";
|
|
3
|
+
import { invalidateCodexModelsCache, syncCatalogModels } from "./catalog/retained-sync";
|
|
4
4
|
import type { ComboCatalogOmission } from "./catalog/aggregation";
|
|
5
5
|
import { getCodexHome } from "./paths";
|
|
6
6
|
import { withCatalogWriteSerialization, CatalogWritePermitRefusal } from "./catalog-write-serialization";
|
|
7
7
|
import { replaceCodexModelsCache } from "./internal/catalog-writer";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
|
-
import type { CodexCatalogSyncOptions } from "./catalog/sync";
|
|
9
|
+
import type { CodexCatalogSyncOptions } from "./catalog/retained-sync";
|
|
10
10
|
|
|
11
11
|
export interface CodexCatalogRefreshResult {
|
|
12
12
|
added: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isCodexAccountGenerationLive } from "../account-store";
|
|
2
2
|
import { NATIVE_RESERVE_MODEL } from "../catalog/native-models";
|
|
3
|
-
import { MAIN_CODEX_ACCOUNT_ID } from "../
|
|
3
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../account-id";
|
|
4
4
|
import { POOL_KEY_CODEX } from "../pool-rotation";
|
|
5
5
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
6
6
|
import type { OcxConfig } from "../../types";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getEffectiveCodexAutoSwitchThreshold } from "../account-auto-switch";
|
|
2
2
|
import { codexAccountUsesCreditsAfterLimit, isCodexAccountHeldForCredits } from "../account-credit-use";
|
|
3
3
|
import { isCodexAccountPaused } from "../account-pause";
|
|
4
|
-
import {
|
|
4
|
+
import { pinnedCodexAccountId } from "../account-priority";
|
|
5
|
+
import { codexSelectionPriorityLookup } from "../account-preservation";
|
|
5
6
|
import { isSelectableCodexPoolAccount } from "../account-id";
|
|
6
7
|
import { isAccountNeedsReauth } from "../account-runtime-state";
|
|
7
8
|
import { isCodexAccountUsable, type CodexAccountUsabilityOptions } from "../account-usability";
|
|
@@ -248,7 +249,7 @@ export function getEligiblePoolAccounts(
|
|
|
248
249
|
const pinned = pinnedCodexAccountId(config);
|
|
249
250
|
return selectPriorityTier(
|
|
250
251
|
withoutModelDeniedAccounts(ids, selectionOptions?.deniedModelAccountIds, pinned),
|
|
251
|
-
|
|
252
|
+
codexSelectionPriorityLookup(config),
|
|
252
253
|
id => hasCodexQuotaHeadroom(config, id, selectionOptions, now),
|
|
253
254
|
pinned,
|
|
254
255
|
);
|
|
@@ -767,7 +768,7 @@ export function pickPriorityPreemption(
|
|
|
767
768
|
&& eligible.includes(pinned)
|
|
768
769
|
&& hasCodexQuotaHeadroom(config, pinned, selectionOptions, now)
|
|
769
770
|
) return null;
|
|
770
|
-
const priorityOf =
|
|
771
|
+
const priorityOf = codexSelectionPriorityLookup(config);
|
|
771
772
|
if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
|
|
772
773
|
// Members without headroom are in the tier only because a sibling has some;
|
|
773
774
|
// picking one would hand the request straight back to a drained account.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCodexAccountGenerationLive, readCodexAccountRecord } from "../account-store";
|
|
2
|
-
import { MAIN_CODEX_ACCOUNT_ID } from "../
|
|
2
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../account-id";
|
|
3
3
|
import { retainedUtf8Bytes } from "../../lib/admission";
|
|
4
4
|
import { clearAllCodexPoolRefreshFailures } from "../pool-refresh-backoff";
|
|
5
5
|
import type { CodexThreadLineage } from "../lineage";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { extractEmail, inspectChatGptDomainClaim } from "../oauth/chatgpt";
|
|
2
|
+
import type { CodexTokens } from "./auth-collision";
|
|
3
|
+
|
|
4
|
+
export type CodexLoginIdentity = Readonly<{ accountId: string; email: string }>;
|
|
5
|
+
let observed: CodexLoginIdentity | undefined;
|
|
6
|
+
|
|
7
|
+
export function codexLoginIdentity(accountId: unknown, email: unknown): CodexLoginIdentity | undefined {
|
|
8
|
+
const member = typeof email === "string" ? email.trim().toLowerCase() : "";
|
|
9
|
+
const workspace = typeof accountId === "string" ? accountId.trim() : "";
|
|
10
|
+
return workspace && member ? { accountId: workspace, email: member } : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Undefined is insufficient evidence; null means conflicting or malformed token evidence. */
|
|
14
|
+
export function codexTokenIdentity(tokens: CodexTokens): CodexLoginIdentity | undefined | null {
|
|
15
|
+
if (typeof tokens.access_token !== "string" || typeof tokens.account_id !== "string"
|
|
16
|
+
|| (tokens.id_token !== undefined && typeof tokens.id_token !== "string")) return null;
|
|
17
|
+
const claims = [tokens.id_token, tokens.access_token]
|
|
18
|
+
.filter((token): token is string => typeof token === "string")
|
|
19
|
+
.map(inspectChatGptDomainClaim);
|
|
20
|
+
if (claims.some(claim => claim.kind === "invalid")) return null;
|
|
21
|
+
const claimIds = claims.flatMap(claim => claim.kind === "valid" ? [claim.accountId] : []);
|
|
22
|
+
const accountId = tokens.account_id.trim() || claimIds[0];
|
|
23
|
+
if (claimIds.some(claim => claim !== accountId)) return null;
|
|
24
|
+
const emails = new Set([extractEmail(tokens.id_token), extractEmail(undefined, tokens.access_token)]
|
|
25
|
+
.flatMap(email => email?.trim() ? [email.trim().toLowerCase()] : []));
|
|
26
|
+
if (emails.size > 1) return null;
|
|
27
|
+
return codexLoginIdentity(accountId, [...emails][0]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Publish only from an existing physical-login observation, never an incoming bearer. */
|
|
31
|
+
export function observeSignedInCodexIdentity(tokens?: CodexTokens): void {
|
|
32
|
+
if (!tokens) { observed = undefined; return; }
|
|
33
|
+
const next = codexTokenIdentity(tokens);
|
|
34
|
+
// Conflicting claims can be a partial external write, never proof of a new login.
|
|
35
|
+
if (next !== null) observed = next;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Routing reads memory only, including while the native profile is fenced. */
|
|
39
|
+
export function getSignedInCodexIdentity(): CodexLoginIdentity | undefined { return observed; }
|
|
40
|
+
|
|
41
|
+
export function sameCodexLogin(a: CodexLoginIdentity, b: CodexLoginIdentity | undefined): boolean {
|
|
42
|
+
return a.accountId === b?.accountId && a.email === b.email;
|
|
43
|
+
}
|
|
@@ -591,7 +591,7 @@ export function maybePrimeSubagentQuota(
|
|
|
591
591
|
if (subagentQuotaPrimeForTests) {
|
|
592
592
|
await subagentQuotaPrimeForTests(config, "subagent-spawn");
|
|
593
593
|
} else {
|
|
594
|
-
const { primeCodexPoolQuotas } = await import("./auth-api");
|
|
594
|
+
const { primeCodexPoolQuotas } = await import("./auth-api/pool-mode-gate");
|
|
595
595
|
await primeCodexPoolQuotas(config, "subagent-spawn");
|
|
596
596
|
}
|
|
597
597
|
quotaPrimedAt.set("global", Date.now());
|