@yansigit/opencodex 2.31.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 +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +205 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,1733 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
atomicWriteFile,
|
|
4
|
+
loadConfig,
|
|
5
|
+
observeConfigGeneration,
|
|
6
|
+
readConfigAdmissionSnapshot,
|
|
7
|
+
subagentDefaultSyncEffective,
|
|
8
|
+
websocketsEnabled,
|
|
9
|
+
} from "../config";
|
|
10
|
+
import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
|
|
11
|
+
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
12
|
+
import { resolveCodexHistoryTransition } from "./history-transition";
|
|
13
|
+
import {
|
|
14
|
+
buildInjectWitness,
|
|
15
|
+
captureCodexPreImages,
|
|
16
|
+
codexInjectLockOutcome,
|
|
17
|
+
codexWriteCoordinationEligibility,
|
|
18
|
+
CodexPartialWriteError,
|
|
19
|
+
CodexWriteConflictError,
|
|
20
|
+
DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
21
|
+
recomputeInjectWitness,
|
|
22
|
+
restoreCodexPreImages,
|
|
23
|
+
} from "./inject-coordination";
|
|
24
|
+
import { readIntegrationRecord } from "./integration-record";
|
|
25
|
+
import { classifyNativeRoutedResidue } from "./native-residue";
|
|
26
|
+
import { inspectNativeCodexOwnership } from "../integrations/native/ownership-preflight";
|
|
27
|
+
import {
|
|
28
|
+
resolveCodexCoordinatorDatabasePath,
|
|
29
|
+
resolveEffectiveUserIdentity,
|
|
30
|
+
} from "./user-identity";
|
|
31
|
+
import {
|
|
32
|
+
markJournalInjectedState,
|
|
33
|
+
journaledInjectedOpenaiBaseUrl,
|
|
34
|
+
journaledInjectedCatalogPath,
|
|
35
|
+
removeJournal,
|
|
36
|
+
restoreJournalState,
|
|
37
|
+
writeJournal,
|
|
38
|
+
} from "./journal";
|
|
39
|
+
import { withCatalogWriteSerialization } from "./catalog-write-serialization";
|
|
40
|
+
import { restoreCodexCatalogWithPermit } from "./catalog/sync";
|
|
41
|
+
import { syncCodexHistoryProvider, type CodexHistoryFailureReason } from "./history-provider";
|
|
42
|
+
import {
|
|
43
|
+
describeHistoryJobFailure,
|
|
44
|
+
deriveCodexHistoryOperation,
|
|
45
|
+
resolveCodexHistoryJobTarget,
|
|
46
|
+
runCodexHistoryJob,
|
|
47
|
+
type CodexHistoryJobOutcome,
|
|
48
|
+
} from "./history-job";
|
|
49
|
+
import {
|
|
50
|
+
OCX_SECTION_MARKER,
|
|
51
|
+
hasInjectedCodexRouting,
|
|
52
|
+
hasInjectedOpenaiBaseUrl,
|
|
53
|
+
isRootOpenaiBaseUrlLine,
|
|
54
|
+
providerTableStart,
|
|
55
|
+
providerTableString,
|
|
56
|
+
rootTomlString,
|
|
57
|
+
stripJournaledOpenaiBaseUrl,
|
|
58
|
+
tomlStringPattern,
|
|
59
|
+
} from "./injected-marker";
|
|
60
|
+
import {
|
|
61
|
+
CODEX_CONFIG_PATH,
|
|
62
|
+
CODEX_PROFILE_PATH,
|
|
63
|
+
DEFAULT_CATALOG_PATH,
|
|
64
|
+
getCodexHome,
|
|
65
|
+
parseTomlString,
|
|
66
|
+
readRootTomlString,
|
|
67
|
+
resolveCodexConfigPath,
|
|
68
|
+
tomlString,
|
|
69
|
+
} from "./paths";
|
|
70
|
+
import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
|
|
71
|
+
import {
|
|
72
|
+
transformManagedSubagentDefaults,
|
|
73
|
+
type ManagedSubagentDefaults,
|
|
74
|
+
} from "./subagent-defaults";
|
|
75
|
+
import { syncCodexAgentRoles } from "./agent-roles-sync";
|
|
76
|
+
import type { OcxConfig } from "../types";
|
|
77
|
+
|
|
78
|
+
// Ownership predicates live in `./injected-marker` so `journal.ts` can reach them
|
|
79
|
+
// without importing this module back. Re-exported for existing external callers.
|
|
80
|
+
export { hasInjectedCodexRouting, hasInjectedOpenaiBaseUrl };
|
|
81
|
+
|
|
82
|
+
export function externalCodexModelProvider(content: string): string | null {
|
|
83
|
+
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
84
|
+
return provider && provider !== "openai" && provider !== "opencodex"
|
|
85
|
+
? provider
|
|
86
|
+
: null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function currentExternalCodexModelProvider(): string | null {
|
|
90
|
+
if (!existsSync(CODEX_CONFIG_PATH)) return null;
|
|
91
|
+
return externalCodexModelProvider(readFileSync(CODEX_CONFIG_PATH, "utf8"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detect the file's dominant line ending. Every transform in this module is LF-pure
|
|
96
|
+
* (split("\n") + hard "\n" joins), so CRLF configs (Windows-edited config.toml) are
|
|
97
|
+
* normalized to LF at the pipeline boundary and converted back on write — otherwise a
|
|
98
|
+
* single inject would leave a mixed-EOL file.
|
|
99
|
+
*/
|
|
100
|
+
export function dominantEol(content: string): "\r\n" | "\n" {
|
|
101
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
102
|
+
if (crlf === 0) return "\n";
|
|
103
|
+
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
104
|
+
return crlf >= bareLf ? "\r\n" : "\n";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Normalize all line endings to `eol` (CRLF first collapsed to LF, then expanded). */
|
|
108
|
+
export function applyEol(content: string, eol: "\r\n" | "\n"): string {
|
|
109
|
+
const lf = content.replace(/\r\n/g, "\n");
|
|
110
|
+
return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Design B (2026-07-06): loopback installs no longer re-tag the provider. Instead of
|
|
115
|
+
* `model_provider = "opencodex"` + a `[model_providers.opencodex]` table, we set the official
|
|
116
|
+
* built-in override `openai_base_url` (codex-rs config_toml.rs) so codex's own `openai`
|
|
117
|
+
* provider points at the proxy. Threads keep `model_provider = "openai"`, so history never
|
|
118
|
+
* needs remapping or restore. Non-loopback binds keep the legacy table injection because the
|
|
119
|
+
* built-in provider cannot carry the `x-opencodex-api-key` env header.
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
export interface InjectCodexOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Absolute or CODEX_HOME-relative catalog path to advertise to Codex. Pass `null` only when the
|
|
125
|
+
* opencodex catalog could not be materialized; Codex will then keep its native catalog instead of
|
|
126
|
+
* failing on a missing model_catalog_json file.
|
|
127
|
+
*/
|
|
128
|
+
catalogPath?: string | null;
|
|
129
|
+
/**
|
|
130
|
+
* How long to wait for the Codex write lock before reporting contention.
|
|
131
|
+
*
|
|
132
|
+
* Bounded by default so a stuck holder cannot wedge `ocx start`; an explicit
|
|
133
|
+
* caller that is willing to wait can raise it.
|
|
134
|
+
*/
|
|
135
|
+
lockTimeoutMs?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Validate the same config transformations and write-coordination eligibility without
|
|
138
|
+
* changing the journal, config, profile, catalog, cache, or history. Sync uses this before
|
|
139
|
+
* provider discovery so a deterministic config refusal cannot degrade an existing catalog.
|
|
140
|
+
*/
|
|
141
|
+
validateOnly?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function configuredManagedSubagentDefaults(
|
|
145
|
+
config:
|
|
146
|
+
| Pick<
|
|
147
|
+
OcxConfig,
|
|
148
|
+
"injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults"
|
|
149
|
+
>
|
|
150
|
+
| undefined,
|
|
151
|
+
): ManagedSubagentDefaults | null {
|
|
152
|
+
if (!subagentDefaultSyncEffective(config ?? {})) return null;
|
|
153
|
+
return {
|
|
154
|
+
model: config!.injectionModel!.trim(),
|
|
155
|
+
...(config!.injectionEffort?.trim()
|
|
156
|
+
? { reasoningEffort: config!.injectionEffort.trim() }
|
|
157
|
+
: {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The `[model_providers.opencodex]` TABLE only. A table is position-independent in TOML, so it is
|
|
163
|
+
* safe to append at EOF. The bare root key `model_provider = "opencodex"` is NOT included here —
|
|
164
|
+
* it must live at the document root (before any table header) and is set separately by
|
|
165
|
+
* setRootModelProvider(). Appending the bare key at EOF was the original bug: it nested under
|
|
166
|
+
* whatever `[table]` happened to be open last (e.g. `[plugins."chrome@openai-bundled"]`), so Codex
|
|
167
|
+
* never saw a global model_provider and silently fell back to the `openai` (ChatGPT) provider.
|
|
168
|
+
*/
|
|
169
|
+
/**
|
|
170
|
+
* True only for hostnames that bind loopback ONLY. Wildcard binds ("0.0.0.0", "::") are NOT
|
|
171
|
+
* loopback: they expose the proxy on every interface and therefore require the admission token.
|
|
172
|
+
* Do not use `providerBaseHost` for this decision — it folds wildcards to 127.0.0.1 because it
|
|
173
|
+
* answers "what address do I dial", which is a different question from "is this exposed".
|
|
174
|
+
*/
|
|
175
|
+
export function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
176
|
+
const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase();
|
|
177
|
+
return (
|
|
178
|
+
normalized === "" ||
|
|
179
|
+
normalized === "localhost" ||
|
|
180
|
+
normalized === "127.0.0.1" ||
|
|
181
|
+
normalized === "::1" ||
|
|
182
|
+
normalized === "[::1]"
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function providerBaseHost(hostname: string | undefined): string {
|
|
187
|
+
const trimmed = (hostname ?? "127.0.0.1").trim();
|
|
188
|
+
const lower = trimmed.toLowerCase();
|
|
189
|
+
// Match what the server actually binds. Writing "localhost" while binding IPv4-only
|
|
190
|
+
// 127.0.0.1 breaks on Windows, where localhost commonly resolves to ::1 first.
|
|
191
|
+
if (lower === "::1" || lower === "[::1]") return "[::1]";
|
|
192
|
+
if (
|
|
193
|
+
isLoopbackHostname(trimmed) ||
|
|
194
|
+
trimmed === "0.0.0.0" ||
|
|
195
|
+
trimmed === "::" ||
|
|
196
|
+
trimmed === "[::]"
|
|
197
|
+
)
|
|
198
|
+
return "127.0.0.1";
|
|
199
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
200
|
+
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function shouldInjectApiAuthHeader(
|
|
204
|
+
config: Pick<OcxConfig, "hostname" | "unauthenticatedLoopbackListener"> | undefined,
|
|
205
|
+
): boolean {
|
|
206
|
+
// The unauthenticated loopback listener is a loopback bind, so it admits without a
|
|
207
|
+
// credential (#1102). Emitting the env header anyway would be worse than useless: the
|
|
208
|
+
// directly-spawned app-server this exists for has no OPENCODEX_API_AUTH_TOKEN in its
|
|
209
|
+
// environment, and Codex would send an empty header value.
|
|
210
|
+
if (config?.unauthenticatedLoopbackListener?.enabled) return false;
|
|
211
|
+
return !isLoopbackHostname(config?.hostname);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function buildProviderTableBlock(
|
|
215
|
+
port: number,
|
|
216
|
+
supportsWebsockets = false,
|
|
217
|
+
includeApiAuthHeader = false,
|
|
218
|
+
hostname?: string,
|
|
219
|
+
): string {
|
|
220
|
+
const host = providerBaseHost(hostname);
|
|
221
|
+
const lines = [
|
|
222
|
+
"",
|
|
223
|
+
OCX_SECTION_MARKER,
|
|
224
|
+
"[model_providers.opencodex]",
|
|
225
|
+
'name = "OpenCodex Proxy"',
|
|
226
|
+
`base_url = "http://${host}:${port}/v1"`,
|
|
227
|
+
'wire_api = "responses"',
|
|
228
|
+
"requires_openai_auth = true",
|
|
229
|
+
];
|
|
230
|
+
if (includeApiAuthHeader) {
|
|
231
|
+
// codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and
|
|
232
|
+
// hard-errors on a missing/empty variable instead of silently omitting auth. It
|
|
233
|
+
// coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the
|
|
234
|
+
// login/account UX), and the server substitutes stored main auth for our admission
|
|
235
|
+
// bearer (#1686), so the modern form is strictly better than the legacy
|
|
236
|
+
// env_http_headers table this line used to emit.
|
|
237
|
+
lines.push('env_key = "OPENCODEX_API_AUTH_TOKEN"');
|
|
238
|
+
}
|
|
239
|
+
if (supportsWebsockets) lines.push("supports_websockets = true");
|
|
240
|
+
return lines.join("\n") + "\n";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function buildOpenaiBaseUrlLine(
|
|
244
|
+
port: number,
|
|
245
|
+
hostname?: string,
|
|
246
|
+
): string {
|
|
247
|
+
return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Design B root-key injection: place `OCX_SECTION_MARKER` + `openai_base_url` at the document
|
|
252
|
+
* ROOT (before the first table header). Idempotent: an existing marker-owned line is rewritten
|
|
253
|
+
* in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
|
|
254
|
+
* and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
|
|
255
|
+
*/
|
|
256
|
+
export function setRootOpenaiBaseUrl(
|
|
257
|
+
content: string,
|
|
258
|
+
port: number,
|
|
259
|
+
hostname?: string,
|
|
260
|
+
): { content: string; keptUserBaseUrl: boolean } {
|
|
261
|
+
const lines = content.split("\n");
|
|
262
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
263
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
264
|
+
const key = buildOpenaiBaseUrlLine(port, hostname);
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
267
|
+
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
268
|
+
const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
|
|
269
|
+
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
270
|
+
lines[i] = key;
|
|
271
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (firstTable === -1) {
|
|
275
|
+
return {
|
|
276
|
+
content:
|
|
277
|
+
content.replace(/\n+$/, "") +
|
|
278
|
+
"\n" +
|
|
279
|
+
OCX_SECTION_MARKER +
|
|
280
|
+
"\n" +
|
|
281
|
+
key +
|
|
282
|
+
"\n",
|
|
283
|
+
keptUserBaseUrl: false,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
let insertAt = firstTable;
|
|
287
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
288
|
+
lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
|
|
289
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
|
|
294
|
+
* A user's own root override (no marker) survives; an orphaned marker with no key line after
|
|
295
|
+
* it is dropped too so repeated strip/inject cycles cannot accumulate marker comments.
|
|
296
|
+
*/
|
|
297
|
+
export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
298
|
+
const lines = content.split("\n");
|
|
299
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
300
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
301
|
+
const drop = new Set<number>();
|
|
302
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
303
|
+
if (!lines[i].includes(OCX_SECTION_MARKER)) continue;
|
|
304
|
+
if (i + 1 < rootEnd && isRootOpenaiBaseUrlLine(lines[i + 1])) {
|
|
305
|
+
drop.add(i);
|
|
306
|
+
drop.add(i + 1);
|
|
307
|
+
} else if (i + 1 >= rootEnd || lines[i + 1].trim() === "") {
|
|
308
|
+
drop.add(i); // orphaned marker at root
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (drop.size === 0) return content;
|
|
312
|
+
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export type CodexRoutingKind =
|
|
316
|
+
"native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
|
|
317
|
+
|
|
318
|
+
type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
319
|
+
|
|
320
|
+
function ipv4Octets(hostname: string): number[] | null {
|
|
321
|
+
const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
322
|
+
if (dotted) {
|
|
323
|
+
const octets = dotted.slice(1).map(Number);
|
|
324
|
+
return octets.some((octet) => octet > 255) ? null : octets;
|
|
325
|
+
}
|
|
326
|
+
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
|
|
327
|
+
if (!mapped) return null;
|
|
328
|
+
const high = Number.parseInt(mapped[1], 16);
|
|
329
|
+
const low = Number.parseInt(mapped[2], 16);
|
|
330
|
+
return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
334
|
+
try {
|
|
335
|
+
const url = new URL(value);
|
|
336
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
|
|
337
|
+
const hostname = url.hostname
|
|
338
|
+
.toLowerCase()
|
|
339
|
+
.replace(/^\[|\]$/g, "")
|
|
340
|
+
.replace(/\.$/, "");
|
|
341
|
+
if (!hostname) return "unknown";
|
|
342
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
343
|
+
return "local";
|
|
344
|
+
if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0")
|
|
345
|
+
return "local";
|
|
346
|
+
const octets = ipv4Octets(hostname);
|
|
347
|
+
if (octets) {
|
|
348
|
+
if (octets.every((octet) => octet === 0)) return "local";
|
|
349
|
+
if (octets[0] === 127) return "local";
|
|
350
|
+
return "remote";
|
|
351
|
+
}
|
|
352
|
+
if (/^::ffff:/i.test(hostname)) return "unknown";
|
|
353
|
+
return "remote";
|
|
354
|
+
} catch {
|
|
355
|
+
return "unknown";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Classify actual routing dependency separately from opencodex ownership. */
|
|
360
|
+
export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
361
|
+
const rootBaseUrl = rootTomlString(content, "openai_base_url");
|
|
362
|
+
if (rootBaseUrl) {
|
|
363
|
+
const endpoint = classifyRoutingEndpoint(rootBaseUrl);
|
|
364
|
+
if (endpoint === "unknown") return "unknown";
|
|
365
|
+
if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local";
|
|
366
|
+
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
367
|
+
}
|
|
368
|
+
const rootProvider = rootTomlString(content, "model_provider");
|
|
369
|
+
if (rootProvider) {
|
|
370
|
+
const providerTableExists =
|
|
371
|
+
providerTableStart(content.split("\n"), rootProvider) !== -1;
|
|
372
|
+
const providerBaseUrl = providerTableString(
|
|
373
|
+
content,
|
|
374
|
+
rootProvider,
|
|
375
|
+
"base_url",
|
|
376
|
+
);
|
|
377
|
+
if (providerBaseUrl) {
|
|
378
|
+
const endpoint = classifyRoutingEndpoint(providerBaseUrl);
|
|
379
|
+
if (endpoint === "unknown") return "unknown";
|
|
380
|
+
if (rootProvider === "opencodex") return "opencodex-local";
|
|
381
|
+
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
382
|
+
}
|
|
383
|
+
if (
|
|
384
|
+
rootProvider === "opencodex" ||
|
|
385
|
+
providerTableExists ||
|
|
386
|
+
rootProvider !== "openai"
|
|
387
|
+
)
|
|
388
|
+
return "unknown";
|
|
389
|
+
}
|
|
390
|
+
return "native";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Read-only probe used by status, doctor, and the dashboard. */
|
|
394
|
+
export function isCodexRoutingInjected(): boolean {
|
|
395
|
+
const path = CODEX_CONFIG_PATH;
|
|
396
|
+
if (!existsSync(path)) return false;
|
|
397
|
+
try {
|
|
398
|
+
return hasInjectedCodexRouting(readFileSync(path, "utf8"));
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function getCodexRoutingKind(): CodexRoutingKind {
|
|
405
|
+
const path = CODEX_CONFIG_PATH;
|
|
406
|
+
if (!existsSync(path)) return "native";
|
|
407
|
+
try {
|
|
408
|
+
return classifyCodexRouting(readFileSync(path, "utf8"));
|
|
409
|
+
} catch {
|
|
410
|
+
return "unknown";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Strip every existing `model_provider` line that we must not duplicate: any line set to
|
|
416
|
+
* "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any
|
|
417
|
+
* ROOT-level model_provider (before the first table) of any value, since we override the global.
|
|
418
|
+
* A `model_provider` legitimately inside a user table/profile with a non-opencodex value is left
|
|
419
|
+
* untouched.
|
|
420
|
+
*/
|
|
421
|
+
function stripExistingModelProvider(content: string): string {
|
|
422
|
+
const lines = content.split("\n");
|
|
423
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
424
|
+
const out: string[] = [];
|
|
425
|
+
lines.forEach((line, i) => {
|
|
426
|
+
if (/^\s*model_provider\s*=/.test(line)) {
|
|
427
|
+
const isOurs = /^\s*model_provider\s*=\s*"opencodex"\s*$/.test(line);
|
|
428
|
+
const isRoot = firstTable === -1 || i < firstTable;
|
|
429
|
+
if (isOurs || isRoot) return; // drop it
|
|
430
|
+
}
|
|
431
|
+
out.push(line);
|
|
432
|
+
});
|
|
433
|
+
return out.join("\n");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Drop ROOT-level `model_context_window` overrides (keys before the first table header). Codex
|
|
438
|
+
* treats this root key as a global override that wins over the per-model catalog values, so a stale
|
|
439
|
+
* `model_context_window = 1000000` makes every model (e.g. gpt-5.5) report a 1M window. User-owned
|
|
440
|
+
* compaction limits do not alter the advertised context window and must survive reinjection.
|
|
441
|
+
*/
|
|
442
|
+
export function stripRootContextWindowOverrides(content: string): string {
|
|
443
|
+
const lines = content.split("\n");
|
|
444
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
445
|
+
return lines
|
|
446
|
+
.filter((line, i) => {
|
|
447
|
+
const isRoot = firstTable === -1 || i < firstTable;
|
|
448
|
+
return !isRoot || !/^\s*model_context_window\s*=/.test(line);
|
|
449
|
+
})
|
|
450
|
+
.join("\n");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function stripRootRoutedModel(content: string): string {
|
|
454
|
+
const lines = content.split("\n");
|
|
455
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
456
|
+
return lines
|
|
457
|
+
.filter((line, i) => {
|
|
458
|
+
const isRoot = firstTable === -1 || i < firstTable;
|
|
459
|
+
if (!isRoot) return true;
|
|
460
|
+
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/);
|
|
461
|
+
if (!m) return true;
|
|
462
|
+
const model = parseTomlString(m[1]);
|
|
463
|
+
return !model?.includes("/");
|
|
464
|
+
})
|
|
465
|
+
.join("\n");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Insert `model_provider = "opencodex"` at the document ROOT — immediately before the first table
|
|
470
|
+
* header (TOML root keys must precede all tables). If there are no tables, append it to the root body.
|
|
471
|
+
*/
|
|
472
|
+
function setRootModelProvider(content: string): string {
|
|
473
|
+
const lines = content.split("\n");
|
|
474
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
475
|
+
const key = 'model_provider = "opencodex"';
|
|
476
|
+
if (firstTable === -1) {
|
|
477
|
+
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
478
|
+
}
|
|
479
|
+
let insertAt = firstTable;
|
|
480
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
481
|
+
lines.splice(insertAt, 0, key);
|
|
482
|
+
return lines.join("\n");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function readRootModelCatalogPath(content: string): string | null {
|
|
486
|
+
const lines = content.split("\n");
|
|
487
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
488
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
489
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
490
|
+
let ownedCatalogPath: string | null = null;
|
|
491
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
492
|
+
const match = modelCatalogAssignment.exec(lines[index]);
|
|
493
|
+
if (!match) continue;
|
|
494
|
+
const catalogPath = parseTomlString(match[1]);
|
|
495
|
+
if (!isOpencodexCatalogPath(catalogPath)) return catalogPath;
|
|
496
|
+
ownedCatalogPath ??= catalogPath;
|
|
497
|
+
}
|
|
498
|
+
return ownedCatalogPath;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function setRootModelCatalogPath(content: string, catalogPath: string): string {
|
|
502
|
+
const lines = content.split("\n");
|
|
503
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
504
|
+
const key = `model_catalog_json = ${tomlString(catalogPath)}`;
|
|
505
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
506
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
507
|
+
const ownedAssignments: number[] = [];
|
|
508
|
+
let hasUserAssignment = false;
|
|
509
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
510
|
+
const m = modelCatalogAssignment.exec(lines[i]);
|
|
511
|
+
if (!m) continue;
|
|
512
|
+
const existing = parseTomlString(m[1]);
|
|
513
|
+
if (isOpencodexCatalogPath(existing)) {
|
|
514
|
+
ownedAssignments.push(i);
|
|
515
|
+
} else {
|
|
516
|
+
hasUserAssignment = true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (hasUserAssignment) {
|
|
520
|
+
const owned = new Set(ownedAssignments);
|
|
521
|
+
return lines.filter((_, index) => !owned.has(index)).join("\n");
|
|
522
|
+
}
|
|
523
|
+
if (ownedAssignments.length > 0) {
|
|
524
|
+
lines[ownedAssignments[0]] = key;
|
|
525
|
+
const duplicates = new Set(ownedAssignments.slice(1));
|
|
526
|
+
return lines.filter((_, index) => !duplicates.has(index)).join("\n");
|
|
527
|
+
}
|
|
528
|
+
if (firstTable === -1) {
|
|
529
|
+
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
530
|
+
}
|
|
531
|
+
let insertAt = firstTable;
|
|
532
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
533
|
+
lines.splice(insertAt, 0, key);
|
|
534
|
+
return lines.join("\n");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function removeProfileSection(content: string): string {
|
|
538
|
+
const lines = content.split("\n");
|
|
539
|
+
const filtered: string[] = [];
|
|
540
|
+
let inProfile = false;
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
if (line.trim() === "[profiles.opencodex]") {
|
|
543
|
+
inProfile = true;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (inProfile) {
|
|
547
|
+
if (/^\s*\[/.test(line) && line.trim() !== "[profiles.opencodex]") {
|
|
548
|
+
inProfile = false;
|
|
549
|
+
filtered.push(line);
|
|
550
|
+
}
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
filtered.push(line);
|
|
554
|
+
}
|
|
555
|
+
return (
|
|
556
|
+
filtered
|
|
557
|
+
.join("\n")
|
|
558
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
559
|
+
.trimEnd() + "\n"
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function normalizeServiceTier(content: string): string {
|
|
564
|
+
return content.replace(
|
|
565
|
+
/^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm,
|
|
566
|
+
'$1"fast"',
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function ensureFastModeFeature(content: string, fastMode?: boolean): string {
|
|
571
|
+
// Tri-state fast mode (see OcxConfig.fastMode): true forces `fast_mode = true`,
|
|
572
|
+
// false forces `fast_mode = false`, and undefined leaves the user's config
|
|
573
|
+
// untouched (no [features] table is added and an existing fast_mode line is
|
|
574
|
+
// preserved as-is). Table and key matching accept the valid TOML spellings
|
|
575
|
+
// `[features] # comment`, `["features"]` / `['features']`, and quoted keys.
|
|
576
|
+
const lines = content.split("\n");
|
|
577
|
+
const featuresHeader = /^\s*\[(["']?)\s*features\s*\1\]\s*(?:#.*)?$/;
|
|
578
|
+
const fastModeKey = /^\s*(?:"fast_mode"|'fast_mode'|fast_mode)\s*=/;
|
|
579
|
+
const featuresStart = lines.findIndex(line => featuresHeader.test(line));
|
|
580
|
+
if (featuresStart === -1) {
|
|
581
|
+
if (fastMode === undefined) return content;
|
|
582
|
+
return content.trimEnd() + "\n\n[features]\nfast_mode = " + (fastMode ? "true" : "false") + "\n";
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const nextTable = lines.findIndex(
|
|
586
|
+
(line, index) => index > featuresStart && /^\s*\[/.test(line),
|
|
587
|
+
);
|
|
588
|
+
const featuresEnd = nextTable === -1 ? lines.length : nextTable;
|
|
589
|
+
for (let i = featuresStart + 1; i < featuresEnd; i++) {
|
|
590
|
+
if (fastModeKey.test(lines[i])) {
|
|
591
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
592
|
+
lines[i] = lines[i].replace(/^(\s*)(?:"fast_mode"|'fast_mode'|fast_mode)\s*=.*$/, `$1fast_mode = ${fastMode ? "true" : "false"}`);
|
|
593
|
+
return lines.join("\n");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
598
|
+
let insertAt = featuresEnd;
|
|
599
|
+
while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
600
|
+
lines.splice(insertAt, 0, `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
601
|
+
return lines.join("\n");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function isOpencodexCatalogPath(path: string): boolean {
|
|
605
|
+
return path.replace(/\\/g, "/").split("/").pop() === "opencodex-catalog.json";
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function stripOpencodexCatalogPath(content: string): string {
|
|
609
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
610
|
+
const lines = content.split("\n");
|
|
611
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
612
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
613
|
+
return lines
|
|
614
|
+
.filter((line, index) => {
|
|
615
|
+
if (index >= rootEnd) return true;
|
|
616
|
+
const m = modelCatalogAssignment.exec(line);
|
|
617
|
+
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
|
|
618
|
+
})
|
|
619
|
+
.join("\n");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean): string {
|
|
623
|
+
const host = providerBaseHost(hostname);
|
|
624
|
+
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
625
|
+
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
626
|
+
// the x-opencodex-api-key env header).
|
|
627
|
+
if (!includeApiAuthHeader) {
|
|
628
|
+
const lines = [
|
|
629
|
+
"# OpenCodex proxy fallback config (Design B)",
|
|
630
|
+
`# Root override that points Codex's built-in openai provider at the proxy on ${host}:${port}.`,
|
|
631
|
+
"# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
|
|
632
|
+
buildOpenaiBaseUrlLine(port, hostname),
|
|
633
|
+
];
|
|
634
|
+
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
635
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
|
|
636
|
+
return lines.join("\n");
|
|
637
|
+
}
|
|
638
|
+
const lines = [
|
|
639
|
+
"# OpenCodex proxy profile — use with: codex --profile opencodex",
|
|
640
|
+
`# Routes all model requests through the opencodex proxy at ${host}:${port}`,
|
|
641
|
+
'model_provider = "opencodex"',
|
|
642
|
+
];
|
|
643
|
+
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
644
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
645
|
+
lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
|
|
646
|
+
return lines.join("\n");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function chooseCatalogPathForInjection(
|
|
650
|
+
content: string,
|
|
651
|
+
requested?: string | null,
|
|
652
|
+
): string | null {
|
|
653
|
+
if (requested !== undefined) return requested;
|
|
654
|
+
|
|
655
|
+
const existing = readRootModelCatalogPath(content);
|
|
656
|
+
if (existing) {
|
|
657
|
+
const resolved = resolveCodexConfigPath(existing);
|
|
658
|
+
if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
|
|
659
|
+
return existing;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export interface CodexInjectResult {
|
|
666
|
+
success: boolean;
|
|
667
|
+
message: string;
|
|
668
|
+
status?: "skipped";
|
|
669
|
+
skippedReason?: "desired_disabled" | "desired_enabled";
|
|
670
|
+
nativeSubagentDefaultsWarning?: string;
|
|
671
|
+
agentRolesSyncWarning?: string;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function injectCodexConfig(
|
|
675
|
+
port: number,
|
|
676
|
+
config?: OcxConfig,
|
|
677
|
+
options: InjectCodexOptions = {},
|
|
678
|
+
): Promise<CodexInjectResult> {
|
|
679
|
+
// Point Codex at the unauthenticated loopback listener when it is enabled (#1102).
|
|
680
|
+
//
|
|
681
|
+
// Resolved here rather than at the call sites because every caller already passes the proxy
|
|
682
|
+
// port and the config together: startup sync, `ocx sync`, and the ensure path would each
|
|
683
|
+
// need the same two-line change, and a caller that missed it would silently emit a base_url
|
|
684
|
+
// requiring a credential the directly-spawned app-server does not have.
|
|
685
|
+
//
|
|
686
|
+
// The listener port is fixed in config, never OS-assigned, so this value survives restarts
|
|
687
|
+
// and matches what an already-running app-server read at startup.
|
|
688
|
+
const loopback = config?.unauthenticatedLoopbackListener;
|
|
689
|
+
if (loopback?.enabled) port = loopback.port;
|
|
690
|
+
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
691
|
+
return {
|
|
692
|
+
success: false,
|
|
693
|
+
message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?`,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Role files are independent of config.toml. Skip when the caller omitted
|
|
698
|
+
// config so an unknown catalog cannot prune owned files. The sync fail-closes
|
|
699
|
+
// internally; do not wrap this call in an empty catch.
|
|
700
|
+
let agentRolesSyncWarning: string | undefined;
|
|
701
|
+
if (!options.validateOnly && config) {
|
|
702
|
+
const roleSync = syncCodexAgentRoles(config);
|
|
703
|
+
if (roleSync.warnings.length > 0) agentRolesSyncWarning = roleSync.warnings.join(" ");
|
|
704
|
+
}
|
|
705
|
+
const withRoleWarning = <T extends CodexInjectResult>(result: T): T =>
|
|
706
|
+
agentRolesSyncWarning ? { ...result, agentRolesSyncWarning } : result;
|
|
707
|
+
|
|
708
|
+
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
709
|
+
const activeProvider = externalCodexModelProvider(rawContent);
|
|
710
|
+
if (activeProvider) {
|
|
711
|
+
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
712
|
+
// replay that stale snapshot over externally managed config.
|
|
713
|
+
if (!options.validateOnly) removeJournal();
|
|
714
|
+
const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(
|
|
715
|
+
config,
|
|
716
|
+
)
|
|
717
|
+
? `Native Codex sub-agent defaults were not injected: external model_provider ${tomlString(activeProvider)} owns config.toml.`
|
|
718
|
+
: undefined;
|
|
719
|
+
return withRoleWarning({
|
|
720
|
+
success: true,
|
|
721
|
+
...(nativeSubagentDefaultsWarning
|
|
722
|
+
? { nativeSubagentDefaultsWarning }
|
|
723
|
+
: {}),
|
|
724
|
+
message:
|
|
725
|
+
`⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
726
|
+
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
727
|
+
` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
|
|
728
|
+
`${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
|
|
729
|
+
` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Marker-owned native defaults are OpenCodex residue, never part of the
|
|
734
|
+
// user's journal baseline. Clean them before either snapshotting or adding a
|
|
735
|
+
// root routing key: inserting that key ahead of a marker-owned first table
|
|
736
|
+
// would otherwise separate the table marker from its header. Ambiguous
|
|
737
|
+
// markers fail closed without writing config, profile, or journal state.
|
|
738
|
+
const nativeDefaultsBaseline = transformManagedSubagentDefaults(
|
|
739
|
+
rawContent,
|
|
740
|
+
null,
|
|
741
|
+
);
|
|
742
|
+
if (!nativeDefaultsBaseline.ok) {
|
|
743
|
+
return {
|
|
744
|
+
success: false,
|
|
745
|
+
message:
|
|
746
|
+
`Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. ` +
|
|
747
|
+
`No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const baselineContent = nativeDefaultsBaseline.content;
|
|
751
|
+
|
|
752
|
+
/*
|
|
753
|
+
* The journal write used to happen HERE, before the transforms. It now happens
|
|
754
|
+
* inside the write lock further down, and the transforms were hoisted above it
|
|
755
|
+
* rather than the lock being narrowed to the three file writes.
|
|
756
|
+
*
|
|
757
|
+
* Why: the lock's witness hashes the CANDIDATE BYTES, and those are not final
|
|
758
|
+
* until `profileContent` and the EOL-applied `content` exist. Opening the lock
|
|
759
|
+
* before them would leave nothing to hash; keeping the journal outside the
|
|
760
|
+
* lock would leave the first artifact-creating write unserialized, which is
|
|
761
|
+
* the hole this edge exists to close.
|
|
762
|
+
*
|
|
763
|
+
* The move is safe because the region between here and the writes performs no
|
|
764
|
+
* filesystem mutation — its only touch is `existsSync` on the catalog paths
|
|
765
|
+
* (`chooseCatalogPathForInjection`) — and because `writeJournal` is called
|
|
766
|
+
* with `configContent`, so it snapshots the baseline it is handed rather than
|
|
767
|
+
* rereading `config.toml` underneath the transforms.
|
|
768
|
+
*/
|
|
769
|
+
// EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
|
|
770
|
+
const eol = dominantEol(rawContent);
|
|
771
|
+
let content = applyEol(baselineContent, "\n");
|
|
772
|
+
|
|
773
|
+
// Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
|
|
774
|
+
// stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
|
|
775
|
+
// table-nested key behind.
|
|
776
|
+
// Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
|
|
777
|
+
// marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
|
|
778
|
+
content = stripInjectedOpenaiBaseUrl(content);
|
|
779
|
+
if (hasOcxProviderTable(content)) {
|
|
780
|
+
content = removeOcxSection(content);
|
|
781
|
+
}
|
|
782
|
+
content = removeProfileSection(content);
|
|
783
|
+
content = stripExistingModelProvider(content);
|
|
784
|
+
content = stripRootContextWindowOverrides(content);
|
|
785
|
+
content = normalizeServiceTier(content);
|
|
786
|
+
content = ensureFastModeFeature(content, config?.fastMode);
|
|
787
|
+
|
|
788
|
+
const catalogPath = chooseCatalogPathForInjection(
|
|
789
|
+
content,
|
|
790
|
+
options.catalogPath,
|
|
791
|
+
);
|
|
792
|
+
content = catalogPath
|
|
793
|
+
? setRootModelCatalogPath(content, catalogPath)
|
|
794
|
+
: stripOpencodexCatalogPath(content);
|
|
795
|
+
|
|
796
|
+
const legacyMode = shouldInjectApiAuthHeader(config);
|
|
797
|
+
let keptUserBaseUrl = false;
|
|
798
|
+
if (legacyMode) {
|
|
799
|
+
// Legacy (non-loopback) injection: the built-in openai provider cannot carry the
|
|
800
|
+
// x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
|
|
801
|
+
// 1) Root key BEFORE the first table header (must be a global, not nested under a table).
|
|
802
|
+
content = setRootModelProvider(content);
|
|
803
|
+
// 2) Provider table appended at EOF (position-independent).
|
|
804
|
+
content =
|
|
805
|
+
content.trimEnd() +
|
|
806
|
+
"\n" +
|
|
807
|
+
buildProviderTableBlock(
|
|
808
|
+
port,
|
|
809
|
+
websocketsEnabled(config ?? {}),
|
|
810
|
+
true,
|
|
811
|
+
config?.hostname,
|
|
812
|
+
);
|
|
813
|
+
} else {
|
|
814
|
+
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
815
|
+
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
816
|
+
content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
|
|
817
|
+
const result = setRootOpenaiBaseUrl(content, port, config?.hostname);
|
|
818
|
+
content = result.content;
|
|
819
|
+
keptUserBaseUrl = result.keptUserBaseUrl;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
|
|
823
|
+
const routingOwnershipWarning =
|
|
824
|
+
keptUserBaseUrl && desiredSubagentDefaults
|
|
825
|
+
? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
|
|
826
|
+
: undefined;
|
|
827
|
+
const managedDefaults = transformManagedSubagentDefaults(
|
|
828
|
+
content,
|
|
829
|
+
keptUserBaseUrl ? null : desiredSubagentDefaults,
|
|
830
|
+
);
|
|
831
|
+
let nativeSubagentDefaultsWarning = routingOwnershipWarning;
|
|
832
|
+
let managedDefaultsMessage = routingOwnershipWarning
|
|
833
|
+
? ` ⚠️ ${routingOwnershipWarning}\n`
|
|
834
|
+
: "";
|
|
835
|
+
if (managedDefaults.ok) {
|
|
836
|
+
content = managedDefaults.content;
|
|
837
|
+
if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
|
|
838
|
+
const keys = managedDefaults.conflicts
|
|
839
|
+
.map((conflict) => `agents.${conflict.key}`)
|
|
840
|
+
.join(", ");
|
|
841
|
+
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
|
|
842
|
+
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
const action =
|
|
846
|
+
desiredSubagentDefaults && !keptUserBaseUrl
|
|
847
|
+
? "were not injected"
|
|
848
|
+
: "could not be safely removed";
|
|
849
|
+
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
|
|
850
|
+
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode);
|
|
854
|
+
content = applyEol(content, eol);
|
|
855
|
+
|
|
856
|
+
/*
|
|
857
|
+
* The witness, built from the FINAL bytes. Everything it hashes is either the
|
|
858
|
+
* output about to be written or evidence that can be re-read under the lock;
|
|
859
|
+
* ownership rides along as recorded context because it is not re-observed
|
|
860
|
+
* there — see `write-coordination.ts`.
|
|
861
|
+
*/
|
|
862
|
+
const persisted = readConfigAdmissionSnapshot();
|
|
863
|
+
const persistedIdentity =
|
|
864
|
+
persisted.kind === "read" ? persisted.contentSha256 : "unreadable";
|
|
865
|
+
const observedGeneration = observeConfigGeneration();
|
|
866
|
+
const generation =
|
|
867
|
+
observedGeneration.kind === "ready"
|
|
868
|
+
? { present: true, value: observedGeneration.generation.value }
|
|
869
|
+
: { present: false, value: 0 };
|
|
870
|
+
const candidate = {
|
|
871
|
+
configBytes: content,
|
|
872
|
+
profileBytes: profileContent,
|
|
873
|
+
catalogPath,
|
|
874
|
+
};
|
|
875
|
+
const witness = buildInjectWitness(
|
|
876
|
+
candidate,
|
|
877
|
+
rawContent,
|
|
878
|
+
persistedIdentity,
|
|
879
|
+
generation,
|
|
880
|
+
"unknown",
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
/*
|
|
884
|
+
* THE COORDINATED SECTION.
|
|
885
|
+
*
|
|
886
|
+
* This is the write lock's first production caller. Everything above is
|
|
887
|
+
* classification and pure transformation; everything from here to the end of
|
|
888
|
+
* the callback replaces files, and two processes doing it at once is the
|
|
889
|
+
* interruption hazard this substrate exists to close.
|
|
890
|
+
*
|
|
891
|
+
* The witness hashes the bytes about to be written rather than the inputs that
|
|
892
|
+
* produced them, so two operations intending different output cannot share an
|
|
893
|
+
* id no matter which input differed.
|
|
894
|
+
*/
|
|
895
|
+
/*
|
|
896
|
+
* Eligibility BEFORE acquisition, never "try and fall back".
|
|
897
|
+
*
|
|
898
|
+
* A home routed before this substrate existed cannot have its first
|
|
899
|
+
* coordinator row created — the guard that refuses is correct — and that
|
|
900
|
+
* describes every pre-substrate install. Attempting the lock there would enter
|
|
901
|
+
* a refusal path on the entire installed base, so the decision happens first
|
|
902
|
+
* and those homes keep the write sequence they have always used.
|
|
903
|
+
*/
|
|
904
|
+
const eligibility = codexWriteCoordinationEligibility({
|
|
905
|
+
coordinatorPath: () =>
|
|
906
|
+
resolveCodexCoordinatorDatabasePath(
|
|
907
|
+
resolveEffectiveUserIdentity(),
|
|
908
|
+
getCodexHome(),
|
|
909
|
+
),
|
|
910
|
+
residue: () => classifyNativeRoutedResidue(),
|
|
911
|
+
integrationRecord: () => readIntegrationRecord(),
|
|
912
|
+
});
|
|
913
|
+
if (eligibility.kind === "refused") {
|
|
914
|
+
return {
|
|
915
|
+
success: false,
|
|
916
|
+
message: `Codex configuration was not written: ${eligibility.reason}.`,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (options.validateOnly) {
|
|
921
|
+
return {
|
|
922
|
+
success: true,
|
|
923
|
+
message: "Codex config injection preflight passed; no files were changed.",
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const applyNativeArtifacts = (): void => {
|
|
928
|
+
writeJournal({
|
|
929
|
+
currentStateIsNative: !hasInjectedCodexRouting(rawContent),
|
|
930
|
+
configContent: baselineContent,
|
|
931
|
+
});
|
|
932
|
+
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
933
|
+
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
934
|
+
markJournalInjectedState(content, profileContent, {
|
|
935
|
+
// A root override is ours only in loopback Design B when no user-owned value won.
|
|
936
|
+
injectedOpenaiBaseUrl: legacyMode || keptUserBaseUrl
|
|
937
|
+
? null
|
|
938
|
+
: rootTomlString(content, "openai_base_url"),
|
|
939
|
+
// This is the catalog artifact selected for this injection, even when config.toml
|
|
940
|
+
// already points at that path and therefore needs no textual rewrite.
|
|
941
|
+
injectedCatalogPath: catalogPath,
|
|
942
|
+
});
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
/*
|
|
946
|
+
* Set only on the coordinated path: the generation/txId the transition just
|
|
947
|
+
* committed. The terminal history update CASes against this, so a job that
|
|
948
|
+
* was overtaken cannot overwrite the winner. Stays undefined for a
|
|
949
|
+
* legacy-uncoordinated home, which publishes no transition to resolve.
|
|
950
|
+
*/
|
|
951
|
+
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
952
|
+
|
|
953
|
+
if (eligibility.kind === "legacy-uncoordinated") {
|
|
954
|
+
// Unchanged behavior for homes the coordinator cannot yet adopt. Stated
|
|
955
|
+
// rather than implied: this is the boundary, and adoption is its own phase.
|
|
956
|
+
if (!shouldSyncCodexOnStart(loadConfig())) {
|
|
957
|
+
return {
|
|
958
|
+
success: true,
|
|
959
|
+
status: "skipped",
|
|
960
|
+
skippedReason: "desired_disabled",
|
|
961
|
+
message: "Codex integration is OFF; no Codex config, catalog, cache, or history was changed.",
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
applyNativeArtifacts();
|
|
965
|
+
} else {
|
|
966
|
+
const coordinated = await withCodexWriteLock(
|
|
967
|
+
{
|
|
968
|
+
timeoutMs: options.lockTimeoutMs ?? DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
969
|
+
admitted: { authoritySnapshotId: witness.comparisonId },
|
|
970
|
+
readAdmissionUnderLock: () => ({
|
|
971
|
+
authoritySnapshotId: recomputeInjectWitness({
|
|
972
|
+
candidate: witness.candidate,
|
|
973
|
+
canonicalTargets: witness.evidence.canonicalTargets,
|
|
974
|
+
persistedIdentity,
|
|
975
|
+
generation,
|
|
976
|
+
observedOwnership: witness.observedOwnership,
|
|
977
|
+
}).comparisonId,
|
|
978
|
+
}),
|
|
979
|
+
},
|
|
980
|
+
(ctx) => {
|
|
981
|
+
if (!shouldSyncCodexOnStart(loadConfig())) {
|
|
982
|
+
throw new CodexWriteLockSkipped("desired_disabled");
|
|
983
|
+
}
|
|
984
|
+
/*
|
|
985
|
+
* Publish BEFORE touching the filesystem. `assertPublished` runs after this
|
|
986
|
+
* callback returns and throws unless a transition was recorded, so writing
|
|
987
|
+
* first would replace every file and only then fail — with SQLite rolling
|
|
988
|
+
* back and the filesystem staying changed.
|
|
989
|
+
*
|
|
990
|
+
* `beginTransition` returns a conflict rather than throwing, so its result
|
|
991
|
+
* is checked here; ignoring it would reach the same failure by a slower
|
|
992
|
+
* route.
|
|
993
|
+
*/
|
|
994
|
+
const published = ctx.coordinator.beginTransition(
|
|
995
|
+
{
|
|
996
|
+
nativeGeneration: ctx.expectation.nativeBefore,
|
|
997
|
+
currentTxId: ctx.currentTxId,
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
txId: ctx.expectation.txId,
|
|
1001
|
+
direction: "apply",
|
|
1002
|
+
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
1003
|
+
nextRetryAt: new Date().toISOString(),
|
|
1004
|
+
},
|
|
1005
|
+
);
|
|
1006
|
+
if (published.kind !== "updated") {
|
|
1007
|
+
throw new CodexWriteConflictError(
|
|
1008
|
+
`The Codex transition could not be published: ${published.kind}.`,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/*
|
|
1013
|
+
* Exact pre-images, captured under the lock and used for compensation.
|
|
1014
|
+
*
|
|
1015
|
+
* A rolled-back coordinator row is not a rolled-back filesystem: each
|
|
1016
|
+
* `atomicWriteFile` is atomic alone, never across the three together, so a
|
|
1017
|
+
* failure partway leaves earlier replacements in place. `restoreJournalState`
|
|
1018
|
+
* cannot be the undo — it restores whichever journal occupies the path,
|
|
1019
|
+
* which need not be the one this operation wrote.
|
|
1020
|
+
*/
|
|
1021
|
+
const preImages = captureCodexPreImages();
|
|
1022
|
+
try {
|
|
1023
|
+
applyNativeArtifacts();
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
// Compensate, then ALWAYS throw. Returning a partial result would let the
|
|
1026
|
+
// lock commit a row describing an apply that did not finish.
|
|
1027
|
+
const restored = restoreCodexPreImages(preImages);
|
|
1028
|
+
if (!restored.complete) {
|
|
1029
|
+
throw new CodexPartialWriteError(restored.unrestored);
|
|
1030
|
+
}
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
kind: "applied" as const,
|
|
1035
|
+
/*
|
|
1036
|
+
* The receipt the terminal update matches on. The transition commits
|
|
1037
|
+
* when the callback returns, so this pair is what the post-job
|
|
1038
|
+
* `updateCodexHistoryTransition` CASes against — an overtaken job
|
|
1039
|
+
* cannot overwrite a winner.
|
|
1040
|
+
*/
|
|
1041
|
+
receipt: {
|
|
1042
|
+
nativeGeneration: ctx.expectation.nativeAfter,
|
|
1043
|
+
currentTxId: ctx.expectation.txId,
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
},
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
if (coordinated.status !== "acquired") {
|
|
1050
|
+
return codexInjectLockOutcome(coordinated);
|
|
1051
|
+
}
|
|
1052
|
+
transitionReceipt = coordinated.value.receipt;
|
|
1053
|
+
}
|
|
1054
|
+
// Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
|
|
1055
|
+
// the opposite: a one-time migration of previously re-tagged threads BACK to openai (restore
|
|
1056
|
+
// machinery; cheap no-op when there is nothing to migrate).
|
|
1057
|
+
// History runs in a Worker under H, not on this thread.
|
|
1058
|
+
//
|
|
1059
|
+
// The three surfaces it touches — the SQLite rows, the backup manifest, and the
|
|
1060
|
+
// rollout files — do not share a transaction, so a busy timeout only ever
|
|
1061
|
+
// serialized one of them and an opposite-direction process could overtake
|
|
1062
|
+
// through the other two. The operation is derived from admitted intent here and
|
|
1063
|
+
// handed down fixed; the Worker never takes a direction from its caller.
|
|
1064
|
+
const historyOutcome = await runCodexHistoryJob({
|
|
1065
|
+
...resolveCodexHistoryJobTarget(),
|
|
1066
|
+
expectedDesiredEnabled: true,
|
|
1067
|
+
operation: deriveCodexHistoryOperation({
|
|
1068
|
+
direction: "apply",
|
|
1069
|
+
resumeHistory: config?.syncResumeHistory !== false,
|
|
1070
|
+
legacyMode,
|
|
1071
|
+
}),
|
|
1072
|
+
});
|
|
1073
|
+
// A blocked or failed unit is reported, not silently counted as zero work:
|
|
1074
|
+
// `failed` is what makes the caller's message say so.
|
|
1075
|
+
const history: { rows: number; files: number; failed?: true } =
|
|
1076
|
+
historyOutcome.kind === "converged"
|
|
1077
|
+
? { rows: historyOutcome.rows, files: historyOutcome.files }
|
|
1078
|
+
: historyOutcome.kind === "skipped"
|
|
1079
|
+
? { rows: 0, files: 0 }
|
|
1080
|
+
: { rows: 0, files: 0, failed: true };
|
|
1081
|
+
|
|
1082
|
+
/*
|
|
1083
|
+
* Resolve the transition this job belongs to, on the coordinated path only.
|
|
1084
|
+
*
|
|
1085
|
+
* `updateCodexHistoryTransition` had no production caller since it was
|
|
1086
|
+
* written, so every completed or skipped job left the row permanently
|
|
1087
|
+
* `pending` — the transition was published and never resolved. This is the
|
|
1088
|
+
* first time the durable row reflects what actually happened. The CAS on the
|
|
1089
|
+
* receipt means an overtaken job's late write loses and is not overwritten.
|
|
1090
|
+
*/
|
|
1091
|
+
if (transitionReceipt) {
|
|
1092
|
+
resolveCodexHistoryTransition(transitionReceipt, historyOutcome);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const catalogMessage = catalogPath
|
|
1096
|
+
? ` Codex model catalog: ${catalogPath}\n`
|
|
1097
|
+
: ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
|
|
1098
|
+
const ejected = (history as { ejectedRows?: number }).ejectedRows ?? 0;
|
|
1099
|
+
const migratedRows = (history.rows ?? 0) + ejected;
|
|
1100
|
+
const historyMessage =
|
|
1101
|
+
config?.syncResumeHistory === false
|
|
1102
|
+
? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
|
|
1103
|
+
: history.failed
|
|
1104
|
+
? formatApplyHistoryFailure(historyOutcome, legacyMode)
|
|
1105
|
+
: legacyMode
|
|
1106
|
+
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
1107
|
+
: migratedRows > 0
|
|
1108
|
+
? ` Codex resume history: ${migratedRows} legacy opencodex-tagged thread(s) migrated back to openai (one-time).\n`
|
|
1109
|
+
: ` Codex resume history: untouched (threads keep their native openai tag).\n`;
|
|
1110
|
+
// A user-owned root openai_base_url means we did NOT install routing — say so honestly
|
|
1111
|
+
// instead of claiming the proxy route is active (catalog/fast_mode were still written).
|
|
1112
|
+
if (keptUserBaseUrl) {
|
|
1113
|
+
return withRoleWarning({
|
|
1114
|
+
success: true,
|
|
1115
|
+
...(nativeSubagentDefaultsWarning
|
|
1116
|
+
? { nativeSubagentDefaultsWarning }
|
|
1117
|
+
: {}),
|
|
1118
|
+
message:
|
|
1119
|
+
`⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
|
|
1120
|
+
catalogMessage +
|
|
1121
|
+
historyMessage +
|
|
1122
|
+
managedDefaultsMessage +
|
|
1123
|
+
` To route plain codex through the proxy, remove your openai_base_url line from ~/.codex/config.toml and rerun 'ocx start'.\n` +
|
|
1124
|
+
` Reference config: ${CODEX_PROFILE_PATH}`,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
const headline = legacyMode
|
|
1128
|
+
? `Injected opencodex as default provider into Codex config.\n`
|
|
1129
|
+
: `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).\n`;
|
|
1130
|
+
return withRoleWarning({
|
|
1131
|
+
success: true,
|
|
1132
|
+
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
1133
|
+
message:
|
|
1134
|
+
headline +
|
|
1135
|
+
catalogMessage +
|
|
1136
|
+
historyMessage +
|
|
1137
|
+
managedDefaultsMessage +
|
|
1138
|
+
` All models now route through opencodex proxy (like OpenRouter).\n` +
|
|
1139
|
+
` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
|
|
1140
|
+
` Custom models route to their configured providers.\n` +
|
|
1141
|
+
(legacyMode
|
|
1142
|
+
? ` Fallback: codex --profile opencodex (same behavior)`
|
|
1143
|
+
: ` Fallback reference: ${CODEX_PROFILE_PATH}`),
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Sub-table headers like `[model_providers.opencodex.env_http_headers]` appear when a Codex app
|
|
1149
|
+
* config rewrite re-serializes the provider's inline `env_http_headers` table. They define the
|
|
1150
|
+
* same `model_providers.opencodex` provider, so cleanup must remove them too — otherwise the
|
|
1151
|
+
* provider survives with no `name` and Codex rejects the whole config
|
|
1152
|
+
* ("provider name must not be empty"). The dot terminator keeps a user's
|
|
1153
|
+
* `[model_providers.opencodex_backup]`-style tables out of scope.
|
|
1154
|
+
*/
|
|
1155
|
+
function isOcxProviderHeaderLine(trimmedLine: string): boolean {
|
|
1156
|
+
// Root form matched by regex, not equality: TOML v1.0 allows a trailing comment
|
|
1157
|
+
// (`[model_providers.opencodex] # comment`), and an exact compare would miss that form.
|
|
1158
|
+
// The sub-table prefix check already tolerates trailing comments by construction.
|
|
1159
|
+
return (
|
|
1160
|
+
/^\[model_providers\.opencodex\]\s*(?:#.*)?$/.test(trimmedLine) ||
|
|
1161
|
+
trimmedLine.startsWith("[model_providers.opencodex.")
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function hasOcxProviderTable(content: string): boolean {
|
|
1166
|
+
return content
|
|
1167
|
+
.split("\n")
|
|
1168
|
+
.some((line) => isOcxProviderHeaderLine(line.trim()));
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function removeOcxSection(content: string): string {
|
|
1172
|
+
const lines = content.split("\n");
|
|
1173
|
+
const filtered: string[] = [];
|
|
1174
|
+
let inOcxSection = false;
|
|
1175
|
+
for (const line of lines) {
|
|
1176
|
+
if (
|
|
1177
|
+
line.includes(OCX_SECTION_MARKER) ||
|
|
1178
|
+
isOcxProviderHeaderLine(line.trim())
|
|
1179
|
+
) {
|
|
1180
|
+
inOcxSection = true;
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (inOcxSection) {
|
|
1184
|
+
// End the injected section at the next table header that ISN'T our own. Exact match on the
|
|
1185
|
+
// provider name (plus our own sub-tables) so a user's
|
|
1186
|
+
// "[model_providers.opencodex_backup]" (or similar) is preserved, not swallowed.
|
|
1187
|
+
if (/^\s*\[/.test(line) && !isOcxProviderHeaderLine(line.trim())) {
|
|
1188
|
+
inOcxSection = false;
|
|
1189
|
+
filtered.push(line);
|
|
1190
|
+
}
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
filtered.push(line);
|
|
1194
|
+
}
|
|
1195
|
+
return (
|
|
1196
|
+
filtered
|
|
1197
|
+
.join("\n")
|
|
1198
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1199
|
+
.trimEnd() + "\n"
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
interface StripOpencodexConfigResult {
|
|
1204
|
+
content: string;
|
|
1205
|
+
managedDefaultsError: string | null;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Detailed form used by the on-disk restore path. A damaged ownership marker is
|
|
1210
|
+
* ambiguous: keep the associated value, but return the transform error so the
|
|
1211
|
+
* caller cannot report a complete restore.
|
|
1212
|
+
*/
|
|
1213
|
+
function stripOpencodexConfigResult(
|
|
1214
|
+
content: string,
|
|
1215
|
+
journaledBaseUrl: string | null = null,
|
|
1216
|
+
): StripOpencodexConfigResult {
|
|
1217
|
+
let out = content;
|
|
1218
|
+
const hadRootOcxProvider =
|
|
1219
|
+
readRootTomlString(out, "model_provider") === "opencodex";
|
|
1220
|
+
// #1798: marker adjacency is FORMATTING evidence, and a Codex app rewrite keeps values
|
|
1221
|
+
// while dropping comments. Fall back to VALUE evidence -- the exact URL we recorded
|
|
1222
|
+
// writing -- so an app-rewritten config is still recognized as ours.
|
|
1223
|
+
const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out)
|
|
1224
|
+
|| (journaledBaseUrl !== null && rootTomlString(out, "openai_base_url") === journaledBaseUrl);
|
|
1225
|
+
out = stripInjectedOpenaiBaseUrl(out); // before removeOcxSection — it keys on the marker line too
|
|
1226
|
+
out = stripJournaledOpenaiBaseUrl(out, journaledBaseUrl);
|
|
1227
|
+
if (hasOcxProviderTable(out)) {
|
|
1228
|
+
out = removeOcxSection(out);
|
|
1229
|
+
}
|
|
1230
|
+
out = removeProfileSection(out);
|
|
1231
|
+
// Regex (not exact-string) removal so compact `model_provider="opencodex"` is stripped too —
|
|
1232
|
+
// must match the detection regex above, or a detected line could survive un-removed.
|
|
1233
|
+
out = out
|
|
1234
|
+
.split("\n")
|
|
1235
|
+
.filter((l) => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l))
|
|
1236
|
+
.join("\n");
|
|
1237
|
+
// Routed root model ids (`model = "provider/slug"`) only make sense while the proxy serves
|
|
1238
|
+
// them — strip on both the legacy re-tag form and the Design B injected-base-url form.
|
|
1239
|
+
if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out);
|
|
1240
|
+
const managedDefaults = transformManagedSubagentDefaults(out, null);
|
|
1241
|
+
if (managedDefaults.ok) out = managedDefaults.content;
|
|
1242
|
+
out = stripOpencodexCatalogPath(out);
|
|
1243
|
+
return {
|
|
1244
|
+
content: out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n",
|
|
1245
|
+
managedDefaultsError: !managedDefaults.ok ? managedDefaults.error : null,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** Pure transform: strip the opencodex provider block + `model_provider = "opencodex"` lines. */
|
|
1250
|
+
export function stripOpencodexConfig(content: string): string {
|
|
1251
|
+
return stripOpencodexConfigResult(content).content;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function hasOpencodexRouting(content: string): boolean {
|
|
1255
|
+
return (
|
|
1256
|
+
hasOcxProviderTable(content) ||
|
|
1257
|
+
/^\s*model_provider\s*=\s*"opencodex"/m.test(content) ||
|
|
1258
|
+
hasInjectedOpenaiBaseUrl(content)
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
export function removeCodexConfig(
|
|
1263
|
+
options: { preserveProfile?: boolean } = {},
|
|
1264
|
+
): { success: boolean; message: string } {
|
|
1265
|
+
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
1266
|
+
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1267
|
+
unlinkSync(CODEX_PROFILE_PATH);
|
|
1268
|
+
return {
|
|
1269
|
+
success: true,
|
|
1270
|
+
message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
1274
|
+
// Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
|
|
1275
|
+
// The unchanged fast path compares in LF space so an untouched file is never rewritten.
|
|
1276
|
+
const eol = dominantEol(rawContent);
|
|
1277
|
+
const content = applyEol(rawContent, "\n");
|
|
1278
|
+
// Read the recorded injection once: the strip below consumes it, and so does the
|
|
1279
|
+
// ownership verdict, which must agree with what was actually removed.
|
|
1280
|
+
const journaledBaseUrl = journaledInjectedOpenaiBaseUrl();
|
|
1281
|
+
const had = hasOpencodexRouting(content)
|
|
1282
|
+
|| (journaledBaseUrl !== null && rootTomlString(content, "openai_base_url") === journaledBaseUrl);
|
|
1283
|
+
const stripped = stripOpencodexConfigResult(content, journaledBaseUrl);
|
|
1284
|
+
if (had || stripped.content !== content) {
|
|
1285
|
+
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped.content, eol));
|
|
1286
|
+
}
|
|
1287
|
+
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1288
|
+
unlinkSync(CODEX_PROFILE_PATH);
|
|
1289
|
+
const removedMessage = had
|
|
1290
|
+
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
|
|
1291
|
+
: "opencodex not present in Codex config.";
|
|
1292
|
+
if (stripped.managedDefaultsError) {
|
|
1293
|
+
const routingMessage = had
|
|
1294
|
+
? removedMessage
|
|
1295
|
+
: "No opencodex routing was present in Codex config.";
|
|
1296
|
+
return {
|
|
1297
|
+
success: false,
|
|
1298
|
+
message:
|
|
1299
|
+
`${routingMessage} Native Codex sub-agent defaults could not be safely removed: ${stripped.managedDefaultsError}. ` +
|
|
1300
|
+
"The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.",
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
success: true,
|
|
1305
|
+
message: removedMessage,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
export type CodexRestoreArtifactState = "ok" | "skipped" | "failed";
|
|
1310
|
+
|
|
1311
|
+
export interface CodexRestoreConfigResult {
|
|
1312
|
+
state: CodexRestoreArtifactState;
|
|
1313
|
+
changed: boolean;
|
|
1314
|
+
action: "journal-restored" | "owned-fields-stripped" | "external-provider-preserved" | "failed";
|
|
1315
|
+
message: string;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export interface CodexRestoreCatalogResult {
|
|
1319
|
+
state: CodexRestoreArtifactState;
|
|
1320
|
+
changed: boolean;
|
|
1321
|
+
removed: number;
|
|
1322
|
+
kept: number;
|
|
1323
|
+
path: string | null;
|
|
1324
|
+
message: string;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
export interface CodexRestoreHistoryResult {
|
|
1328
|
+
state: CodexRestoreArtifactState;
|
|
1329
|
+
changed: boolean;
|
|
1330
|
+
reason?: CodexHistoryFailureReason;
|
|
1331
|
+
rows: number;
|
|
1332
|
+
files: number;
|
|
1333
|
+
ejectedRows: number;
|
|
1334
|
+
message: string;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export interface CodexNativeRestoreResult {
|
|
1338
|
+
success: boolean;
|
|
1339
|
+
message: string;
|
|
1340
|
+
externalProvider?: string;
|
|
1341
|
+
artifacts: {
|
|
1342
|
+
config: CodexRestoreConfigResult;
|
|
1343
|
+
catalog: CodexRestoreCatalogResult;
|
|
1344
|
+
history: CodexRestoreHistoryResult;
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function failedHistoryRestore(reason?: CodexHistoryFailureReason, detail?: string): CodexRestoreHistoryResult {
|
|
1349
|
+
return {
|
|
1350
|
+
state: "failed",
|
|
1351
|
+
changed: false,
|
|
1352
|
+
...(reason ? { reason } : {}),
|
|
1353
|
+
rows: 0,
|
|
1354
|
+
files: 0,
|
|
1355
|
+
ejectedRows: 0,
|
|
1356
|
+
message: reason === "permission"
|
|
1357
|
+
? "Codex resume history could NOT be restored because permission was denied."
|
|
1358
|
+
: reason === "busy"
|
|
1359
|
+
? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
|
|
1360
|
+
: detail
|
|
1361
|
+
? `Codex resume history could NOT be restored: ${detail}`
|
|
1362
|
+
: "Codex resume history could NOT be restored; the reason was not recorded. Run 'ocx doctor'.",
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Restore failure wording for a Worker outcome.
|
|
1368
|
+
*
|
|
1369
|
+
* Only a genuine busy result blames the Codex app. An unsafe-path refusal, an
|
|
1370
|
+
* unavailable coordinator database, a permission denial, or a dead/timed-out
|
|
1371
|
+
* worker is a different problem; the old collapse made every one of those read
|
|
1372
|
+
* as "the Codex app is holding the database" (issue #1191). `busy` and
|
|
1373
|
+
* `permission` keep the restore-specific sentence built by
|
|
1374
|
+
* `failedHistoryRestore`; every other reason reuses the single formatter so
|
|
1375
|
+
* the two modules cannot drift apart.
|
|
1376
|
+
*/
|
|
1377
|
+
export function failedHistoryRestoreFromOutcome(
|
|
1378
|
+
outcome: Extract<CodexHistoryJobOutcome, { kind: "blocked" | "failed" }>,
|
|
1379
|
+
): CodexRestoreHistoryResult {
|
|
1380
|
+
if (outcome.kind === "blocked" && outcome.reason === "busy") return failedHistoryRestore("busy");
|
|
1381
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") return failedHistoryRestore("busy");
|
|
1382
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "permission") {
|
|
1383
|
+
return failedHistoryRestore("permission");
|
|
1384
|
+
}
|
|
1385
|
+
return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function externalProviderRestoreResult(activeProvider: string): CodexNativeRestoreResult {
|
|
1389
|
+
const message = `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`;
|
|
1390
|
+
return {
|
|
1391
|
+
success: true,
|
|
1392
|
+
message,
|
|
1393
|
+
externalProvider: activeProvider,
|
|
1394
|
+
artifacts: {
|
|
1395
|
+
config: { state: "skipped", changed: false, action: "external-provider-preserved", message },
|
|
1396
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1397
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1398
|
+
},
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/** A foreign service claim is an authority boundary, including explicit CLI restore. */
|
|
1403
|
+
function foreignOwnershipRestoreRefusal(message: string): CodexNativeRestoreResult {
|
|
1404
|
+
return {
|
|
1405
|
+
success: false,
|
|
1406
|
+
message: `Codex native restore refused: ${message}`,
|
|
1407
|
+
artifacts: {
|
|
1408
|
+
config: { state: "skipped", changed: false, action: "failed", message },
|
|
1409
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1410
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1411
|
+
},
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function desiredEnabledRestoreSkip(): CodexNativeRestoreResult {
|
|
1416
|
+
const message = "Codex integration was re-enabled; native restore was skipped.";
|
|
1417
|
+
return skippedRestoreEnvelope(true, message);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* A schema-complete all-skipped envelope for outcomes decided before any
|
|
1422
|
+
* restore machinery runs. Every `restore --json` path must stay shape-stable
|
|
1423
|
+
* with `CodexNativeRestoreResult`; consumers never special-case early exits.
|
|
1424
|
+
*/
|
|
1425
|
+
export function skippedRestoreEnvelope(success: boolean, message: string): CodexNativeRestoreResult {
|
|
1426
|
+
return {
|
|
1427
|
+
success,
|
|
1428
|
+
message,
|
|
1429
|
+
artifacts: {
|
|
1430
|
+
config: { state: "skipped", changed: false, action: "owned-fields-stripped", message },
|
|
1431
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1432
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1433
|
+
},
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/** The config/profile half of a native restore, reported as one artifact. */
|
|
1438
|
+
function restoreCodexConfigInline(): CodexRestoreConfigResult {
|
|
1439
|
+
try {
|
|
1440
|
+
const journal = restoreJournalState();
|
|
1441
|
+
const restored = journal.configRestored
|
|
1442
|
+
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
1443
|
+
: removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
|
|
1444
|
+
return restored.success
|
|
1445
|
+
? {
|
|
1446
|
+
state: "ok",
|
|
1447
|
+
changed: journal.configRestored || journal.profileRestored || journal.profileChanged || restored.message.startsWith("Removed"),
|
|
1448
|
+
action: journal.configRestored ? "journal-restored" : "owned-fields-stripped",
|
|
1449
|
+
message: restored.message,
|
|
1450
|
+
}
|
|
1451
|
+
: { state: "failed", changed: false, action: "failed", message: restored.message };
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
return { state: "failed", changed: false, action: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** The catalog half, always inside its own K acquisition. */
|
|
1458
|
+
/**
|
|
1459
|
+
* The catalog half, always inside its own K acquisition.
|
|
1460
|
+
*
|
|
1461
|
+
* `journaledCatalogPath` must be captured by the CALLER, before the config half runs: a
|
|
1462
|
+
* successful journal restore deletes the journal, and a config restore can remove
|
|
1463
|
+
* `model_catalog_json`. Reading it here would be too late in both cases (#1798).
|
|
1464
|
+
*/
|
|
1465
|
+
function restoreCodexCatalogArtifact(
|
|
1466
|
+
revalidateDesiredState: boolean,
|
|
1467
|
+
journaledCatalogPath: string | null,
|
|
1468
|
+
): CodexRestoreCatalogResult {
|
|
1469
|
+
const owningCodexHome = getCodexHome();
|
|
1470
|
+
try {
|
|
1471
|
+
const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
1472
|
+
revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
|
|
1473
|
+
? null
|
|
1474
|
+
: restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath));
|
|
1475
|
+
return restored.kind === "completed" && restored.value !== null
|
|
1476
|
+
? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
|
|
1477
|
+
: restored.kind === "completed"
|
|
1478
|
+
? {
|
|
1479
|
+
state: "skipped", changed: false, removed: 0, kept: 0, path: null,
|
|
1480
|
+
message: "Codex integration was re-enabled; native catalog restoration was skipped.",
|
|
1481
|
+
}
|
|
1482
|
+
: {
|
|
1483
|
+
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
1484
|
+
message: `Codex catalog could not be restored: ${restored.reason}.`,
|
|
1485
|
+
};
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
return {
|
|
1488
|
+
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
1489
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Restore native Codex, running history in a Worker under H.
|
|
1496
|
+
*
|
|
1497
|
+
* On a coordinated home the config/profile restore happens INSIDE the Codex
|
|
1498
|
+
* write lock, publishing a `remove` transition — the same serialization inject
|
|
1499
|
+
* uses. Without it, an older restore could overwrite a config a concurrent
|
|
1500
|
+
* enable had just written under the lock, and then honestly report success
|
|
1501
|
+
* while desired intent said ON. The desired-state re-read under the lock turns
|
|
1502
|
+
* that lost race into the discriminated `desired_enabled` skip.
|
|
1503
|
+
*/
|
|
1504
|
+
export async function restoreNativeCodexAsync(
|
|
1505
|
+
options: { revalidateDesiredState?: boolean } = {},
|
|
1506
|
+
): Promise<CodexNativeRestoreResult> {
|
|
1507
|
+
const activeProvider = currentExternalCodexModelProvider();
|
|
1508
|
+
if (activeProvider) {
|
|
1509
|
+
// External-provider courtesy: only the stale journal is removed. The
|
|
1510
|
+
// history worker must not launch — it would turn a read-mostly courtesy
|
|
1511
|
+
// result into a history mutation on a home we do not own.
|
|
1512
|
+
removeJournal();
|
|
1513
|
+
return externalProviderRestoreResult(activeProvider);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// `restore` normally honours a human request even when an unrelated
|
|
1517
|
+
// service-manager probe is unavailable. A recorded FOREIGN home is not an
|
|
1518
|
+
// unrelated probe: it is positive evidence another installation owns these
|
|
1519
|
+
// native artifacts, so do not create profile/claim locks before refusing.
|
|
1520
|
+
if (options.revalidateDesiredState) {
|
|
1521
|
+
const ownership = inspectNativeCodexOwnership();
|
|
1522
|
+
if (ownership.ownership === "foreign") return foreignOwnershipRestoreRefusal(ownership.reason);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const eligibility = codexWriteCoordinationEligibility({
|
|
1526
|
+
coordinatorPath: () =>
|
|
1527
|
+
resolveCodexCoordinatorDatabasePath(resolveEffectiveUserIdentity(), getCodexHome()),
|
|
1528
|
+
residue: () => classifyNativeRoutedResidue(),
|
|
1529
|
+
integrationRecord: () => readIntegrationRecord(),
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
1533
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
1534
|
+
// catalog we actually wrote (#1798).
|
|
1535
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
1536
|
+
let config: CodexRestoreConfigResult;
|
|
1537
|
+
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
1538
|
+
|
|
1539
|
+
if (eligibility.kind === "coordinated") {
|
|
1540
|
+
// The restore has no candidate bytes to witness; freshness comes from the
|
|
1541
|
+
// filesystem reads and the desired-state re-read performed under the lock.
|
|
1542
|
+
const witness = { authoritySnapshotId: "codex-native-restore" };
|
|
1543
|
+
const coordinated = await withCodexWriteLock(
|
|
1544
|
+
{
|
|
1545
|
+
timeoutMs: DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
1546
|
+
admitted: witness,
|
|
1547
|
+
readAdmissionUnderLock: () => witness,
|
|
1548
|
+
},
|
|
1549
|
+
(ctx) => {
|
|
1550
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
1551
|
+
throw new CodexWriteLockSkipped("desired_enabled");
|
|
1552
|
+
}
|
|
1553
|
+
const published = ctx.coordinator.beginTransition(
|
|
1554
|
+
{
|
|
1555
|
+
nativeGeneration: ctx.expectation.nativeBefore,
|
|
1556
|
+
currentTxId: ctx.currentTxId,
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
txId: ctx.expectation.txId,
|
|
1560
|
+
direction: "remove",
|
|
1561
|
+
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
1562
|
+
nextRetryAt: new Date().toISOString(),
|
|
1563
|
+
},
|
|
1564
|
+
);
|
|
1565
|
+
if (published.kind !== "updated") {
|
|
1566
|
+
throw new CodexWriteConflictError(
|
|
1567
|
+
`The Codex transition could not be published: ${published.kind}.`,
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
const preImages = captureCodexPreImages();
|
|
1571
|
+
let restored: CodexRestoreConfigResult;
|
|
1572
|
+
try {
|
|
1573
|
+
restored = restoreCodexConfigInline();
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
const compensated = restoreCodexPreImages(preImages);
|
|
1576
|
+
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
config: restored,
|
|
1581
|
+
receipt: {
|
|
1582
|
+
nativeGeneration: ctx.expectation.nativeAfter,
|
|
1583
|
+
currentTxId: ctx.expectation.txId,
|
|
1584
|
+
},
|
|
1585
|
+
};
|
|
1586
|
+
},
|
|
1587
|
+
);
|
|
1588
|
+
if (coordinated.status === "skipped") return desiredEnabledRestoreSkip();
|
|
1589
|
+
if (coordinated.status !== "acquired") {
|
|
1590
|
+
config = {
|
|
1591
|
+
state: "failed",
|
|
1592
|
+
changed: false,
|
|
1593
|
+
action: "failed",
|
|
1594
|
+
message: coordinated.status === "busy"
|
|
1595
|
+
? `Another process is writing Codex configuration right now (waited ${coordinated.waitedMs}ms). Retry shortly.`
|
|
1596
|
+
: `Codex configuration was not restored: ${coordinated.message}`,
|
|
1597
|
+
};
|
|
1598
|
+
} else {
|
|
1599
|
+
config = coordinated.value.config;
|
|
1600
|
+
transitionReceipt = coordinated.value.receipt;
|
|
1601
|
+
}
|
|
1602
|
+
} else {
|
|
1603
|
+
// Legacy-uncoordinated (or unresolvable) homes keep the unserialized path
|
|
1604
|
+
// they have always had; restore is the escape hatch and must not strand
|
|
1605
|
+
// them. The plain re-read still honors an intervening re-enable.
|
|
1606
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
1607
|
+
return desiredEnabledRestoreSkip();
|
|
1608
|
+
}
|
|
1609
|
+
config = restoreCodexConfigInline();
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
1613
|
+
const outcome = await runCodexHistoryJob({
|
|
1614
|
+
...resolveCodexHistoryJobTarget(),
|
|
1615
|
+
...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
|
|
1616
|
+
operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
|
|
1617
|
+
});
|
|
1618
|
+
if (transitionReceipt) {
|
|
1619
|
+
resolveCodexHistoryTransition(transitionReceipt, outcome);
|
|
1620
|
+
}
|
|
1621
|
+
const history: CodexRestoreHistoryResult = outcome.kind === "converged"
|
|
1622
|
+
? {
|
|
1623
|
+
state: "ok", changed: outcome.rows > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
1624
|
+
message: outcome.rows > 0
|
|
1625
|
+
? `Resume history restored from opencodex backup (${outcome.rows} thread(s)).`
|
|
1626
|
+
: "Codex resume history was already native.",
|
|
1627
|
+
}
|
|
1628
|
+
: outcome.kind === "skipped"
|
|
1629
|
+
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "Codex resume history was skipped." }
|
|
1630
|
+
: outcome.kind === "blocked" && (outcome.reason === "desired_disabled" || outcome.reason === "desired_enabled")
|
|
1631
|
+
? {
|
|
1632
|
+
state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
|
|
1633
|
+
message: outcome.reason === "desired_disabled"
|
|
1634
|
+
? "Codex integration was disabled; history restoration was skipped."
|
|
1635
|
+
: "Codex integration was enabled; history restoration was skipped.",
|
|
1636
|
+
}
|
|
1637
|
+
: outcome.kind === "blocked" || outcome.kind === "failed"
|
|
1638
|
+
? failedHistoryRestoreFromOutcome(outcome)
|
|
1639
|
+
: failedHistoryRestore();
|
|
1640
|
+
const base = catalog.removed > 0
|
|
1641
|
+
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
1642
|
+
: config.message;
|
|
1643
|
+
const success = config.state !== "failed"
|
|
1644
|
+
&& catalog.state !== "failed"
|
|
1645
|
+
&& history.state !== "failed";
|
|
1646
|
+
return {
|
|
1647
|
+
success,
|
|
1648
|
+
message: `${base}${history.state === "failed" ? ` ⚠️ ${history.message}` : ""}`,
|
|
1649
|
+
artifacts: { config, catalog, history },
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateDesiredState?: boolean } = {}): CodexNativeRestoreResult {
|
|
1654
|
+
const activeProvider = currentExternalCodexModelProvider();
|
|
1655
|
+
if (activeProvider) {
|
|
1656
|
+
removeJournal();
|
|
1657
|
+
return externalProviderRestoreResult(activeProvider);
|
|
1658
|
+
}
|
|
1659
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
1660
|
+
return desiredEnabledRestoreSkip();
|
|
1661
|
+
}
|
|
1662
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
1663
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
1664
|
+
// catalog we actually wrote (#1798).
|
|
1665
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
1666
|
+
const config = restoreCodexConfigInline();
|
|
1667
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
1668
|
+
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
1669
|
+
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
1670
|
+
// (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
|
|
1671
|
+
// Legacy (non-loopback) installs keep the unconditional write-open restore.
|
|
1672
|
+
let skipWhenProvablyNoop = false;
|
|
1673
|
+
try {
|
|
1674
|
+
skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
|
|
1675
|
+
} catch {
|
|
1676
|
+
/* unreadable config: keep the conservative write-open restore */
|
|
1677
|
+
}
|
|
1678
|
+
// `skipHistory` is how the async wrapper takes this work for itself: the
|
|
1679
|
+
// native files come down here, and history runs in the Worker under H.
|
|
1680
|
+
const rawHistory = options.skipHistory
|
|
1681
|
+
? { rows: 0, files: 0 }
|
|
1682
|
+
: syncCodexHistoryProvider("openai", undefined, undefined, {
|
|
1683
|
+
skipWhenProvablyNoop,
|
|
1684
|
+
});
|
|
1685
|
+
const history: CodexRestoreHistoryResult = options.skipHistory
|
|
1686
|
+
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
|
|
1687
|
+
: rawHistory.failed
|
|
1688
|
+
? failedHistoryRestore(rawHistory.failureReason)
|
|
1689
|
+
: {
|
|
1690
|
+
state: "ok",
|
|
1691
|
+
changed: rawHistory.rows > 0 || (rawHistory.ejectedRows ?? 0) > 0,
|
|
1692
|
+
rows: rawHistory.rows,
|
|
1693
|
+
files: rawHistory.files,
|
|
1694
|
+
ejectedRows: rawHistory.ejectedRows ?? 0,
|
|
1695
|
+
message: rawHistory.rows > 0
|
|
1696
|
+
? `Resume history restored from opencodex backup (${rawHistory.rows} thread(s)).`
|
|
1697
|
+
: "Codex resume history was already native.",
|
|
1698
|
+
};
|
|
1699
|
+
const message = catalog.removed > 0
|
|
1700
|
+
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
1701
|
+
: config.message;
|
|
1702
|
+
return {
|
|
1703
|
+
success: config.state !== "failed" && catalog.state !== "failed" && history.state !== "failed",
|
|
1704
|
+
message,
|
|
1705
|
+
artifacts: { config, catalog, history },
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
export function getCodexConfigPath(): string {
|
|
1710
|
+
return CODEX_CONFIG_PATH;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Frame one failed apply history job honestly.
|
|
1715
|
+
*
|
|
1716
|
+
* A genuine lock keeps the established deferred/SKIPPED wording; any other
|
|
1717
|
+
* reason names itself instead of blaming the Codex app/IDE.
|
|
1718
|
+
*/
|
|
1719
|
+
export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legacyMode: boolean): string {
|
|
1720
|
+
// A busy database is a deferral no matter which half observed it: the lock
|
|
1721
|
+
// contended (blocked/busy), or the worker acquired the lock and then found
|
|
1722
|
+
// SQLite busy (failed with a busy history reason). Only those keep the
|
|
1723
|
+
// deferred headline; every other failure is a real "NOT changed".
|
|
1724
|
+
const busy =
|
|
1725
|
+
(outcome.kind === "blocked" && outcome.reason === "busy") ||
|
|
1726
|
+
(outcome.kind === "failed" && outcome.historyFailureReason === "busy");
|
|
1727
|
+
const headline = legacyMode
|
|
1728
|
+
? "Codex resume history sync SKIPPED"
|
|
1729
|
+
: busy
|
|
1730
|
+
? "Codex resume history migration deferred"
|
|
1731
|
+
: "Codex resume history NOT changed";
|
|
1732
|
+
return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
|
|
1733
|
+
}
|