@iislee/opencodex 2.11.0 → 2.35.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/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -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/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- 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 +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- 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 +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -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 +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -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 +622 -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 +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- 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-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -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/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -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/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- 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 +33 -5
- 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.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- 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 +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -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 +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- 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 +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- 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 +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- 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 +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- 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 +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- 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/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- 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 +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- 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 +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- 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 +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-layers.ts — the Codex prompt-layer surface in `$CODEX_HOME/config.toml`.
|
|
3
|
+
*
|
|
4
|
+
* Scope boundary: this module owns the five `include_*` prompt toggles and the
|
|
5
|
+
* generated `developer_instructions` projection. It is a SIBLING of
|
|
6
|
+
* `features.ts`, not an extension of it: that module's header explicitly forbids
|
|
7
|
+
* broadening itself past `multi_agent_v2`, so the technique is copied here
|
|
8
|
+
* rather than the file being widened.
|
|
9
|
+
*
|
|
10
|
+
* Two design decisions are load-bearing and were forced by an adversarial audit
|
|
11
|
+
* (devlog/_plan/260802_codex_set_prompt_composer/):
|
|
12
|
+
*
|
|
13
|
+
* 1. NO USER PROSE IS PARSED BACK OUT OF TOML. Custom layers live in
|
|
14
|
+
* `opencodex-prompt.json`, which we own outright; config.toml receives a
|
|
15
|
+
* write-only projection of the enabled subset. Layer identity never has to
|
|
16
|
+
* survive a round trip through a TOML parser.
|
|
17
|
+
*
|
|
18
|
+
* 2. NO TOML LIBRARY IS USED TO VERIFY WHAT WE WROTE. Measured on Bun 1.3.14,
|
|
19
|
+
* `Bun.TOML.parse` transposes `\t` and `\f`, rejects `\u0007`, and does not
|
|
20
|
+
* trim the newline after an opening `'''`. Codex parses with Rust
|
|
21
|
+
* `toml_edit`, so verifying through a JS parser could report success on a
|
|
22
|
+
* file Codex reads differently. Instead the accepted character set is
|
|
23
|
+
* restricted until escaping is total under three rules, and verification is
|
|
24
|
+
* a byte comparison.
|
|
25
|
+
*
|
|
26
|
+
* CODEX_HOME is resolved at CALL time (the `features.ts:58-67` pattern) so tests
|
|
27
|
+
* can point fixtures via env or an explicit path.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
30
|
+
import { dirname, join, resolve } from "node:path";
|
|
31
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
32
|
+
import { expandUserPath } from "../config";
|
|
33
|
+
import { CODEX_CONFIG_PATH } from "./paths";
|
|
34
|
+
import { OCX_SECTION_MARKER } from "./injected-marker";
|
|
35
|
+
import {
|
|
36
|
+
durableWrite,
|
|
37
|
+
durableWriteExclusive,
|
|
38
|
+
durableDelete,
|
|
39
|
+
encodeJournal,
|
|
40
|
+
ensureDir,
|
|
41
|
+
hashBytes,
|
|
42
|
+
recoverIfNeeded as recoverJournal,
|
|
43
|
+
type JournalRecord,
|
|
44
|
+
} from "./prompt-journal";
|
|
45
|
+
import { release, stillHeld, tryAcquire } from "./prompt-lock";
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Inventory — ONE definition, consumed by the route and the GUI alike.
|
|
49
|
+
// Classes are the five in devlog 001 §4; the partition is total and disjoint.
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
export type LayerClass =
|
|
53
|
+
| "base"
|
|
54
|
+
| "config-toggle"
|
|
55
|
+
| "feature-gated"
|
|
56
|
+
| "runtime-conditional"
|
|
57
|
+
| "extension-unknown";
|
|
58
|
+
|
|
59
|
+
export type ToggleId =
|
|
60
|
+
| "permissions"
|
|
61
|
+
| "collaboration"
|
|
62
|
+
| "environment"
|
|
63
|
+
| "apps"
|
|
64
|
+
| "skills";
|
|
65
|
+
|
|
66
|
+
export interface LayerDescriptor {
|
|
67
|
+
id: string;
|
|
68
|
+
class: LayerClass;
|
|
69
|
+
/** config key for config-toggle and feature-gated rows; null otherwise */
|
|
70
|
+
key: string | null;
|
|
71
|
+
/** documented default when the key is absent */
|
|
72
|
+
default: boolean | null;
|
|
73
|
+
/** assembly index from devlog 001 §1; null when registration-order dependent */
|
|
74
|
+
order: number | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Assembly order per `core/src/session/world_state.rs`. `base-instructions` is
|
|
79
|
+
* NOT a world-state section — it travels in the Responses `instructions` field
|
|
80
|
+
* — so it carries order 0 and sits ahead of the rest.
|
|
81
|
+
*
|
|
82
|
+
* `plugins` is `runtime-conditional`, not feature-gated: `core/src/mcp.rs:200`
|
|
83
|
+
* computes `selected_plugin_available || !capability_summaries().is_empty()`,
|
|
84
|
+
* so `[features] plugins` influences the right operand but does not gate
|
|
85
|
+
* emission.
|
|
86
|
+
*/
|
|
87
|
+
export const LAYER_INVENTORY: readonly LayerDescriptor[] = Object.freeze([
|
|
88
|
+
{ id: "base-instructions", class: "base", key: null, default: null, order: 0 },
|
|
89
|
+
{ id: "model-switch", class: "runtime-conditional", key: null, default: null, order: 1 },
|
|
90
|
+
{ id: "personality", class: "feature-gated", key: "features.personality", default: true, order: 2 },
|
|
91
|
+
{ id: "context-window-guidance", class: "feature-gated", key: "features.token_budget", default: false, order: 3 },
|
|
92
|
+
{ id: "realtime", class: "runtime-conditional", key: null, default: null, order: 4 },
|
|
93
|
+
{ id: "agents-md", class: "runtime-conditional", key: null, default: null, order: 5 },
|
|
94
|
+
{ id: "permissions", class: "config-toggle", key: "include_permissions_instructions", default: true, order: 6 },
|
|
95
|
+
{ id: "collaboration", class: "config-toggle", key: "include_collaboration_mode_instructions", default: true, order: 7 },
|
|
96
|
+
{ id: "environment", class: "config-toggle", key: "include_environment_context", default: true, order: 8 },
|
|
97
|
+
{ id: "environments-instructions", class: "feature-gated", key: "features.deferred_executor", default: false, order: 9 },
|
|
98
|
+
{ id: "apps", class: "config-toggle", key: "include_apps_instructions", default: true, order: 10 },
|
|
99
|
+
{ id: "plugins", class: "runtime-conditional", key: null, default: null, order: 11 },
|
|
100
|
+
{ id: "tools", class: "feature-gated", key: "features.deferred_tool_world_state", default: false, order: 12 },
|
|
101
|
+
{ id: "skills", class: "config-toggle", key: "skills.include_instructions", default: true, order: 13 },
|
|
102
|
+
{ id: "multi-agent-mode", class: "feature-gated", key: "features.multi_agent_v2.enabled", default: false, order: 14 },
|
|
103
|
+
/**
|
|
104
|
+
* Commit and pull-request attribution, contributed by `ext/git-attribution` rather
|
|
105
|
+
* than by a world_state.rs section — which is why it is absent from the order list
|
|
106
|
+
* above and carries `order: null`: it registers through
|
|
107
|
+
* `extensions.context_contributors()` (`core/src/session/world_state.rs:64-66`),
|
|
108
|
+
* whose position is registration-order dependent.
|
|
109
|
+
*
|
|
110
|
+
* `runtime-conditional`, NOT feature-gated. `ext/git-attribution/src/lib.rs:33-80`
|
|
111
|
+
* resolves enablement from the AUTH SERVER via `resolve_attribution_policy`, caches
|
|
112
|
+
* it on the thread store, and falls back to disabled when the lookup fails.
|
|
113
|
+
* `features/src/lib.rs:277` records the old config flag as removed, so there is no
|
|
114
|
+
* key for this GUI to write and nothing in [features] to point a user at.
|
|
115
|
+
*
|
|
116
|
+
* Both states emit text: enabled sends the `Co-authored-by: Codex` trailer plus the
|
|
117
|
+
* `Generated with Codex.` PR marker, disabled sends an explicit countermand. So the
|
|
118
|
+
* row's condition line must name the policy rather than claiming "always on".
|
|
119
|
+
*/
|
|
120
|
+
{ id: "git-attribution", class: "runtime-conditional", key: null, default: null, order: null },
|
|
121
|
+
] as const);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The write allowlist. Fixed, never computed: `config_toml.rs` does NOT carry
|
|
125
|
+
* serde's `deny_unknown_fields`, so a typo'd key is silently ignored in normal
|
|
126
|
+
* mode and a hard startup error under `--strict-config`. A fixed table means
|
|
127
|
+
* the GUI can never emit a key it did not intend.
|
|
128
|
+
*/
|
|
129
|
+
const TOGGLE_KEYS: Record<ToggleId, { table: string | null; key: string }> = {
|
|
130
|
+
permissions: { table: null, key: "include_permissions_instructions" },
|
|
131
|
+
collaboration: { table: null, key: "include_collaboration_mode_instructions" },
|
|
132
|
+
environment: { table: null, key: "include_environment_context" },
|
|
133
|
+
apps: { table: null, key: "include_apps_instructions" },
|
|
134
|
+
skills: { table: "skills", key: "include_instructions" },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const TOGGLE_IDS = Object.freeze(Object.keys(TOGGLE_KEYS) as ToggleId[]);
|
|
138
|
+
|
|
139
|
+
export function isToggleId(value: string): value is ToggleId {
|
|
140
|
+
return Object.prototype.hasOwnProperty.call(TOGGLE_KEYS, value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Paths
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
export interface Paths {
|
|
148
|
+
configPath?: string;
|
|
149
|
+
storePath?: string;
|
|
150
|
+
baseVariantDir?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function activeCodexHome(): string {
|
|
154
|
+
const raw = process.env.CODEX_HOME?.trim();
|
|
155
|
+
if (!raw) return CODEX_CONFIG_PATH.slice(0, -"/config.toml".length);
|
|
156
|
+
const path = resolve(expandUserPath(raw));
|
|
157
|
+
try {
|
|
158
|
+
return realpathSync.native(path);
|
|
159
|
+
} catch {
|
|
160
|
+
return path;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function activeConfigPath(opts?: Paths): string {
|
|
165
|
+
return opts?.configPath ?? join(activeCodexHome(), "config.toml");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function activeStorePath(opts?: Paths): string {
|
|
169
|
+
return opts?.storePath ?? join(activeCodexHome(), "opencodex-prompt.json");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Where authored base-prompt variants live, one markdown file per variant.
|
|
174
|
+
*
|
|
175
|
+
* A directory of real files rather than another JSON store, because
|
|
176
|
+
* `model_instructions_file` points Codex at a path it reads directly. Embedding the
|
|
177
|
+
* bodies in `opencodex-prompt.json` would mean materialising a temp file at selection
|
|
178
|
+
* time, which is a second write path for no gain.
|
|
179
|
+
*/
|
|
180
|
+
export function activeBaseVariantDir(opts?: Paths): string {
|
|
181
|
+
return opts?.baseVariantDir ?? join(activeCodexHome(), "opencodex-prompt-base");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function journalPathFor(storePath: string): string {
|
|
185
|
+
return `${storePath.replace(/\.json$/, "")}.journal`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function lockPathFor(storePath: string): string {
|
|
189
|
+
return `${storePath.replace(/\.json$/, "")}.lock`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Character policy — see the header. Defined over Unicode SCALAR VALUES, not
|
|
194
|
+
// UTF-16 code units, because a lone surrogate is not a scalar value and UTF-8
|
|
195
|
+
// encoding would silently substitute U+FFFD.
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
export interface CharacterFinding {
|
|
199
|
+
/** code-point index, consistent across module, route and editor */
|
|
200
|
+
position: number;
|
|
201
|
+
reason: "control" | "unpaired-surrogate";
|
|
202
|
+
codePoint: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Tab to four spaces, CRLF and lone CR to LF. Applied BEFORE validation. */
|
|
206
|
+
export function normalizeBody(body: string): string {
|
|
207
|
+
return body.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** First offending scalar, or null. Run AFTER normalizeBody. */
|
|
211
|
+
export function findInvalidCharacter(body: string): CharacterFinding | null {
|
|
212
|
+
let position = 0;
|
|
213
|
+
for (let i = 0; i < body.length; ) {
|
|
214
|
+
const code = body.codePointAt(i)!;
|
|
215
|
+
const unit = body.charCodeAt(i);
|
|
216
|
+
const isHighSurrogate = unit >= 0xd800 && unit <= 0xdbff;
|
|
217
|
+
const isLowSurrogate = unit >= 0xdc00 && unit <= 0xdfff;
|
|
218
|
+
// codePointAt only combines a well-formed pair, so a surviving surrogate
|
|
219
|
+
// code point here is unpaired by construction.
|
|
220
|
+
if ((isHighSurrogate || isLowSurrogate) && code === unit) {
|
|
221
|
+
return { position, reason: "unpaired-surrogate", codePoint: code };
|
|
222
|
+
}
|
|
223
|
+
const isNewline = code === 0x0a;
|
|
224
|
+
const isC0 = code < 0x20 && !isNewline;
|
|
225
|
+
const isDel = code === 0x7f;
|
|
226
|
+
const isC1 = code >= 0x80 && code <= 0x9f;
|
|
227
|
+
if (isC0 || isDel || isC1) {
|
|
228
|
+
return { position, reason: "control", codePoint: code };
|
|
229
|
+
}
|
|
230
|
+
i += code > 0xffff ? 2 : 1;
|
|
231
|
+
position += 1;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* TOML basic-string encoding, total over the accepted set: three rules, none of
|
|
238
|
+
* them in the range where `Bun.TOML.parse` misbehaves. `\r` cannot appear
|
|
239
|
+
* because normalizeBody removed it; control characters cannot appear because
|
|
240
|
+
* findInvalidCharacter rejected them.
|
|
241
|
+
*/
|
|
242
|
+
export function encodeBasicString(body: string): string {
|
|
243
|
+
return `"${body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Inverse of `encodeBasicString`, deliberately narrow: it accepts ONLY the three
|
|
248
|
+
* escapes we emit. `\t`, `\f`, `\b`, `\r` and `\uXXXX` are refused rather than
|
|
249
|
+
* guessed — decoding them correctly is exactly the ambiguity the restricted set
|
|
250
|
+
* exists to avoid.
|
|
251
|
+
*/
|
|
252
|
+
export function decodeBasicString(literal: string): string | null {
|
|
253
|
+
if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null;
|
|
254
|
+
const inner = literal.slice(1, -1);
|
|
255
|
+
let out = "";
|
|
256
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
257
|
+
const ch = inner[i]!;
|
|
258
|
+
if (ch !== "\\") {
|
|
259
|
+
if (ch === '"') return null; // unescaped quote: not a single literal
|
|
260
|
+
out += ch;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const next = inner[i + 1];
|
|
264
|
+
if (next === "\\") out += "\\";
|
|
265
|
+
else if (next === '"') out += '"';
|
|
266
|
+
else if (next === "n") out += "\n";
|
|
267
|
+
else return null; // any other escape is outside what we will decode
|
|
268
|
+
i += 1;
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Byte-level hashing. The revision covers COMPLETE file bytes plus existence,
|
|
275
|
+
// so removing the marker while leaving the value intact still changes it.
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
function readFileOrNull(path: string): string | null {
|
|
279
|
+
try {
|
|
280
|
+
if (!existsSync(path)) return null;
|
|
281
|
+
return readFileSync(path, "utf8");
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function computeRevision(configBytes: string | null, storeBytes: string | null): string {
|
|
288
|
+
const hash = createHash("sha256");
|
|
289
|
+
hash.update("cfg:");
|
|
290
|
+
hash.update(configBytes ?? "\0absent");
|
|
291
|
+
hash.update("\nstore:");
|
|
292
|
+
hash.update(storeBytes ?? "\0absent");
|
|
293
|
+
return `sha256:${hash.digest("hex")}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { readFileOrNull as readFileBytes };
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Scoped TOML scanning. Line-based like `features.ts:80-93`: booleans need no
|
|
300
|
+
// escaping, and line editing preserves the user's comments and formatting
|
|
301
|
+
// exactly where a re-serialize would not.
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
const TABLE_HEADER = /^\s*\[/;
|
|
305
|
+
|
|
306
|
+
/** Lines of the root scope: everything before the first `[table]` header. */
|
|
307
|
+
function rootLines(content: string): string[] {
|
|
308
|
+
const lines = content.split("\n");
|
|
309
|
+
const first = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
310
|
+
return first === -1 ? lines : lines.slice(0, first);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Lines of `[header]`'s body, up to the next table header. */
|
|
314
|
+
function tableLines(content: string, header: string): string[] | null {
|
|
315
|
+
const lines = content.split("\n");
|
|
316
|
+
const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
317
|
+
const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
|
|
318
|
+
if (start === -1) return null;
|
|
319
|
+
const rest = lines.slice(start + 1);
|
|
320
|
+
const end = rest.findIndex(l => TABLE_HEADER.test(l));
|
|
321
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function boolInLines(lines: string[], key: string): boolean | null {
|
|
325
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
326
|
+
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(true|false)\\s*(?:#.*)?$`);
|
|
327
|
+
for (const line of lines) {
|
|
328
|
+
const m = pattern.exec(line);
|
|
329
|
+
if (m) return m[1] === "true";
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Ownership of the generated projection.
|
|
336
|
+
//
|
|
337
|
+
// Canonical physical form, always exactly two lines at the top of the document:
|
|
338
|
+
//
|
|
339
|
+
// # Auto-injected by opencodex
|
|
340
|
+
// developer_instructions = "<single-line basic string>"
|
|
341
|
+
//
|
|
342
|
+
// Replacement is "find the marker, replace the next line" — never a span search.
|
|
343
|
+
// Adjacency mirrors `injected-marker.ts:53-60`, tightened by a shape check.
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
const DEV_INSTRUCTIONS_KEY = "developer_instructions";
|
|
347
|
+
const CANONICAL_LINE = /^developer_instructions = "(?:[^"\\]|\\.)*"$/;
|
|
348
|
+
const ANY_DEV_INSTRUCTIONS = /^\s*(?:developer_instructions|"developer_instructions"|'developer_instructions')\s*=/;
|
|
349
|
+
|
|
350
|
+
export type Ownership =
|
|
351
|
+
/** no such key anywhere in the root scope */
|
|
352
|
+
| { state: "absent" }
|
|
353
|
+
/** marker-adjacent and canonically shaped: ours to rewrite */
|
|
354
|
+
| { state: "owned"; line: number; literal: string }
|
|
355
|
+
/** marker-adjacent but reshaped: refuse, offer repair */
|
|
356
|
+
| { state: "owned-malformed"; line: number; raw: string }
|
|
357
|
+
/** no marker: externally authored, refuse and offer adoption */
|
|
358
|
+
| { state: "external"; line: number; raw: string };
|
|
359
|
+
|
|
360
|
+
export function inspectOwnership(configBytes: string | null): Ownership {
|
|
361
|
+
if (configBytes === null) return { state: "absent" };
|
|
362
|
+
const lines = rootLines(configBytes);
|
|
363
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
364
|
+
const raw = lines[i]!;
|
|
365
|
+
if (!ANY_DEV_INSTRUCTIONS.test(raw)) continue;
|
|
366
|
+
const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
|
|
367
|
+
if (!marked) return { state: "external", line: i + 1, raw };
|
|
368
|
+
if (!CANONICAL_LINE.test(raw)) return { state: "owned-malformed", line: i + 1, raw };
|
|
369
|
+
const literal = raw.slice(`${DEV_INSTRUCTIONS_KEY} = `.length);
|
|
370
|
+
return { state: "owned", line: i + 1, literal };
|
|
371
|
+
}
|
|
372
|
+
return { state: "absent" };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Store — the single source of truth for custom layers.
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
export interface CustomLayer {
|
|
380
|
+
/** [a-z0-9]{6}, stable across edits */
|
|
381
|
+
id: string;
|
|
382
|
+
title: string;
|
|
383
|
+
body: string;
|
|
384
|
+
enabled: boolean;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const LAYER_ID = /^[a-z0-9]{6}$/;
|
|
388
|
+
|
|
389
|
+
function isCustomLayer(value: unknown): value is CustomLayer {
|
|
390
|
+
if (!value || typeof value !== "object") return false;
|
|
391
|
+
const v = value as Record<string, unknown>;
|
|
392
|
+
return typeof v.id === "string" && LAYER_ID.test(v.id)
|
|
393
|
+
&& typeof v.title === "string"
|
|
394
|
+
&& typeof v.body === "string"
|
|
395
|
+
&& typeof v.enabled === "boolean";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** null means unreadable/malformed, which is NOT the same as an empty store. */
|
|
399
|
+
export function parseStore(storeBytes: string | null): CustomLayer[] | null {
|
|
400
|
+
if (storeBytes === null) return null;
|
|
401
|
+
let parsed: unknown;
|
|
402
|
+
try {
|
|
403
|
+
parsed = JSON.parse(storeBytes);
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
408
|
+
const layers = (parsed as { layers?: unknown }).layers;
|
|
409
|
+
if (!Array.isArray(layers) || !layers.every(isCustomLayer)) return null;
|
|
410
|
+
const ids = new Set(layers.map(l => l.id));
|
|
411
|
+
if (ids.size !== layers.length) return null;
|
|
412
|
+
return layers;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Enabled layers, joined in order. This is the value written to config.toml. */
|
|
416
|
+
export function composeProjection(layers: readonly CustomLayer[]): string {
|
|
417
|
+
return layers.filter(l => l.enabled).map(l => l.body).join("\n\n");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Snapshot
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
export interface ToggleState {
|
|
425
|
+
id: ToggleId;
|
|
426
|
+
key: string;
|
|
427
|
+
/** null = the key is absent from the user file */
|
|
428
|
+
userFileValue: boolean | null;
|
|
429
|
+
/**
|
|
430
|
+
* userFileValue ?? default. NOT the resolved Codex value: opencodex reads one
|
|
431
|
+
* of the eight config layers, so it reports this file's value under a name
|
|
432
|
+
* that says as much.
|
|
433
|
+
*/
|
|
434
|
+
defaultedUserValue: boolean;
|
|
435
|
+
default: boolean;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export type Drift =
|
|
439
|
+
| "journal-present"
|
|
440
|
+
| "projection-stale"
|
|
441
|
+
| "store-missing"
|
|
442
|
+
| "owned-malformed"
|
|
443
|
+
| null;
|
|
444
|
+
|
|
445
|
+
/** One authored base-prompt variant. `default` is never represented here. */
|
|
446
|
+
export interface BaseVariant {
|
|
447
|
+
id: string;
|
|
448
|
+
title: string;
|
|
449
|
+
body: string;
|
|
450
|
+
bytes: number;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Which base prompt is in force. THREE values, not two.
|
|
455
|
+
*
|
|
456
|
+
* - `default` — `model_instructions_file` is absent, so Codex uses its own base prompt.
|
|
457
|
+
* This is the absence of a key, not a body we store: there is nothing to edit and
|
|
458
|
+
* nothing to delete, which is what makes the default structurally immutable rather
|
|
459
|
+
* than merely guarded.
|
|
460
|
+
* - a variant id — the key points inside our own variant directory.
|
|
461
|
+
* - `external` — the key is set and points somewhere else.
|
|
462
|
+
*
|
|
463
|
+
* The third value is load-bearing and an audit forced it. Collapsing it into `default`
|
|
464
|
+
* would have shown a user "Codex's own base prompt" while their base prompt was in fact
|
|
465
|
+
* replaced by a file they had set by hand. The panel already ships a notice for that
|
|
466
|
+
* state in ten locales; this keeps reporting it instead of overwriting a key we do not
|
|
467
|
+
* own.
|
|
468
|
+
*/
|
|
469
|
+
export type BaseSelection = { kind: "default" } | { kind: "variant"; id: string } | { kind: "external"; path: string };
|
|
470
|
+
|
|
471
|
+
export interface PromptLayerSnapshot {
|
|
472
|
+
configPath: string;
|
|
473
|
+
storePath: string;
|
|
474
|
+
configExists: boolean;
|
|
475
|
+
readable: boolean;
|
|
476
|
+
developerInstructionsOwned: boolean;
|
|
477
|
+
/** non-null blocks mutations until resolved */
|
|
478
|
+
drift: Drift;
|
|
479
|
+
toggles: ToggleState[];
|
|
480
|
+
custom: CustomLayer[];
|
|
481
|
+
modelInstructionsFile: string | null;
|
|
482
|
+
baseVariants: BaseVariant[];
|
|
483
|
+
baseSelection: BaseSelection;
|
|
484
|
+
revision: string;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function readToggle(configBytes: string | null, id: ToggleId): ToggleState {
|
|
488
|
+
const spec = TOGGLE_KEYS[id];
|
|
489
|
+
const descriptor = LAYER_INVENTORY.find(d => d.id === id)!;
|
|
490
|
+
const fallback = descriptor.default ?? true;
|
|
491
|
+
const key = spec.table ? `${spec.table}.${spec.key}` : spec.key;
|
|
492
|
+
let value: boolean | null = null;
|
|
493
|
+
if (configBytes !== null) {
|
|
494
|
+
const scope = spec.table ? tableLines(configBytes, spec.table) : rootLines(configBytes);
|
|
495
|
+
value = scope === null ? null : boolInLines(scope, spec.key);
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
id,
|
|
499
|
+
key,
|
|
500
|
+
userFileValue: value,
|
|
501
|
+
defaultedUserValue: value ?? fallback,
|
|
502
|
+
default: fallback,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function readModelInstructionsFile(configBytes: string | null): string | null {
|
|
507
|
+
if (configBytes === null) return null;
|
|
508
|
+
for (const line of rootLines(configBytes)) {
|
|
509
|
+
const m = /^\s*model_instructions_file\s*=\s*"([^"]*)"\s*(?:#.*)?$/.exec(line);
|
|
510
|
+
if (m) return m[1]!;
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Variant ids are ours to generate, so they stay in one narrow shape. */
|
|
516
|
+
const BASE_VARIANT_ID = /^[a-z0-9]{6}$/;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* The variant files on disk, newest-id-last so the picker order is stable.
|
|
520
|
+
*
|
|
521
|
+
* `default.md` is SKIPPED rather than read: `default` names the absence of a key, so a
|
|
522
|
+
* file claiming that id would appear as a fourth variant whose selection could never be
|
|
523
|
+
* expressed. Anything not matching our own id shape is skipped for the same reason - we
|
|
524
|
+
* only report what we could also write.
|
|
525
|
+
*/
|
|
526
|
+
export function readBaseVariants(opts?: Paths): BaseVariant[] {
|
|
527
|
+
const dir = activeBaseVariantDir(opts);
|
|
528
|
+
let names: string[];
|
|
529
|
+
try {
|
|
530
|
+
names = readdirSync(dir);
|
|
531
|
+
} catch {
|
|
532
|
+
// Absent directory is an ordinary first run, not an error.
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
const out: BaseVariant[] = [];
|
|
536
|
+
for (const name of names.sort()) {
|
|
537
|
+
if (!name.endsWith(".md")) continue;
|
|
538
|
+
const id = name.slice(0, -3);
|
|
539
|
+
if (!BASE_VARIANT_ID.test(id)) continue;
|
|
540
|
+
const body = readFileOrNull(join(dir, name));
|
|
541
|
+
if (body === null) continue;
|
|
542
|
+
// First line is the title when it is a markdown heading; the rest is the prompt.
|
|
543
|
+
// Storing the title inside the file keeps one artifact per variant instead of a
|
|
544
|
+
// sidecar index that can disagree with it.
|
|
545
|
+
const nl = body.indexOf("\n");
|
|
546
|
+
const firstLine = nl === -1 ? body : body.slice(0, nl);
|
|
547
|
+
const titled = firstLine.startsWith("# ");
|
|
548
|
+
out.push({
|
|
549
|
+
id,
|
|
550
|
+
title: titled ? firstLine.slice(2).trim() : id,
|
|
551
|
+
body: titled ? body.slice(nl === -1 ? body.length : nl + 1) : body,
|
|
552
|
+
bytes: Buffer.byteLength(body, "utf8"),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Resolve which base prompt is in force, given the config bytes and the variants.
|
|
560
|
+
*
|
|
561
|
+
* Comparison is by RESOLVED path: `~/.codex/opencodex-prompt-base/abc123.md` and an
|
|
562
|
+
* absolute spelling of the same file are the same selection, and treating them as
|
|
563
|
+
* different would report `external` for a variant we wrote ourselves.
|
|
564
|
+
*/
|
|
565
|
+
export function resolveBaseSelection(
|
|
566
|
+
configBytes: string | null,
|
|
567
|
+
variants: readonly BaseVariant[],
|
|
568
|
+
opts?: Paths,
|
|
569
|
+
): BaseSelection {
|
|
570
|
+
const raw = readModelInstructionsFile(configBytes);
|
|
571
|
+
if (raw === null) return { kind: "default" };
|
|
572
|
+
const dir = activeBaseVariantDir(opts);
|
|
573
|
+
let resolved: string;
|
|
574
|
+
try {
|
|
575
|
+
resolved = resolve(expandUserPath(raw));
|
|
576
|
+
} catch {
|
|
577
|
+
return { kind: "external", path: raw };
|
|
578
|
+
}
|
|
579
|
+
for (const variant of variants) {
|
|
580
|
+
if (resolved === resolve(join(dir, `${variant.id}.md`))) {
|
|
581
|
+
return { kind: "variant", id: variant.id };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return { kind: "external", path: raw };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Pure. Never writes, never locks, never recovers — a GET must not modify a
|
|
589
|
+
* user's configuration, so drift is REPORTED here and resolved elsewhere.
|
|
590
|
+
*/
|
|
591
|
+
export function readPromptLayers(opts?: Paths): PromptLayerSnapshot {
|
|
592
|
+
const configPath = activeConfigPath(opts);
|
|
593
|
+
const storePath = activeStorePath(opts);
|
|
594
|
+
const configExists = existsSync(configPath);
|
|
595
|
+
const configBytes = readFileOrNull(configPath);
|
|
596
|
+
const storeExists = existsSync(storePath);
|
|
597
|
+
const storeBytes = readFileOrNull(storePath);
|
|
598
|
+
|
|
599
|
+
// Present but unreadable is a hard stop; absent is an ordinary first run.
|
|
600
|
+
const readable = !configExists || configBytes !== null;
|
|
601
|
+
const ownership = inspectOwnership(configBytes);
|
|
602
|
+
const layers = parseStore(storeBytes);
|
|
603
|
+
const projection = ownership.state === "owned"
|
|
604
|
+
? decodeBasicString(ownership.literal)
|
|
605
|
+
: null;
|
|
606
|
+
const baseVariants = readBaseVariants(opts);
|
|
607
|
+
|
|
608
|
+
let drift: Drift = null;
|
|
609
|
+
if (existsSync(`${storePath.replace(/\.json$/, "")}.journal`)) {
|
|
610
|
+
drift = "journal-present";
|
|
611
|
+
} else if (ownership.state === "owned-malformed") {
|
|
612
|
+
drift = "owned-malformed";
|
|
613
|
+
} else if (!storeExists && projection !== null && projection.length > 0) {
|
|
614
|
+
// The store is gone while a live projection remains. Treating this as an
|
|
615
|
+
// empty store would erase the active prompt on the next write.
|
|
616
|
+
drift = "store-missing";
|
|
617
|
+
} else if (layers !== null && projection !== null && composeProjection(layers) !== projection) {
|
|
618
|
+
drift = "projection-stale";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
configPath,
|
|
623
|
+
storePath,
|
|
624
|
+
configExists,
|
|
625
|
+
readable,
|
|
626
|
+
developerInstructionsOwned: ownership.state === "owned",
|
|
627
|
+
drift,
|
|
628
|
+
toggles: TOGGLE_IDS.map(id => readToggle(configBytes, id)),
|
|
629
|
+
custom: layers ?? [],
|
|
630
|
+
modelInstructionsFile: readModelInstructionsFile(configBytes),
|
|
631
|
+
baseVariants,
|
|
632
|
+
baseSelection: resolveBaseSelection(configBytes, baseVariants, opts),
|
|
633
|
+
revision: computeRevision(configBytes, storeBytes),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// Writing
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
export type WriteError =
|
|
642
|
+
| "config_unreadable"
|
|
643
|
+
| "stale_revision"
|
|
644
|
+
| "developer_instructions_not_owned"
|
|
645
|
+
| "unknown_layer"
|
|
646
|
+
| "store_unreadable"
|
|
647
|
+
| "invalid_characters"
|
|
648
|
+
| "write_superseded"
|
|
649
|
+
// The filesystem refused a rename that passed every precondition: a directory on
|
|
650
|
+
// the store path, a mode change, a full disk. Distinct from write_superseded,
|
|
651
|
+
// which means another writer won a race — here nobody won and nothing landed.
|
|
652
|
+
| "write_failed"
|
|
653
|
+
| "recovery_required"
|
|
654
|
+
| "locked";
|
|
655
|
+
|
|
656
|
+
export type WriteResult =
|
|
657
|
+
| { ok: true; changed: boolean; snapshot: PromptLayerSnapshot }
|
|
658
|
+
| { ok: false; error: WriteError; detail?: string };
|
|
659
|
+
|
|
660
|
+
/** Line editing, not re-serialization: the user's comments and layout survive. */
|
|
661
|
+
function dominantEol(content: string): "\r\n" | "\n" {
|
|
662
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
663
|
+
if (crlf === 0) return "\n";
|
|
664
|
+
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
665
|
+
return crlf >= bareLf ? "\r\n" : "\n";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function splitLines(content: string): string[] {
|
|
669
|
+
return content.replace(/\r\n/g, "\n").split("\n");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* A leading UTF-8 BOM, split off so line editing never steps over it.
|
|
674
|
+
*
|
|
675
|
+
* Codex reads config.toml with Rust `toml_edit`, which accepts a BOM at byte 0 and
|
|
676
|
+
* nowhere else. Inserting the generated block at line index 0 pushed the BOM down
|
|
677
|
+
* to byte 58, the write reported success because our own byte comparison matched
|
|
678
|
+
* what we intended to write, and the next parse failed with
|
|
679
|
+
* "Expected a key but found (0xEF)" — a config file the user could no longer load,
|
|
680
|
+
* produced by a write that told them it worked.
|
|
681
|
+
*
|
|
682
|
+
* Editors on Windows write this byte routinely, so the file is not exotic.
|
|
683
|
+
*/
|
|
684
|
+
function splitBom(content: string): { bom: string; body: string } {
|
|
685
|
+
return content.startsWith("\ufeff")
|
|
686
|
+
? { bom: "\ufeff", body: content.slice(1) }
|
|
687
|
+
: { bom: "", body: content };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function joinLines(lines: string[], eol: "\r\n" | "\n"): string {
|
|
691
|
+
const text = lines.join("\n");
|
|
692
|
+
return eol === "\n" ? text : text.replace(/\n/g, "\r\n");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function firstTableIndex(lines: string[]): number {
|
|
696
|
+
const idx = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
697
|
+
return idx === -1 ? lines.length : idx;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Set a root-scope boolean, inserting above the first table when absent. */
|
|
701
|
+
function setRootBool(content: string, key: string, value: boolean): string {
|
|
702
|
+
const eol = dominantEol(content);
|
|
703
|
+
const { bom, body } = splitBom(content);
|
|
704
|
+
const lines = splitLines(body);
|
|
705
|
+
const limit = firstTableIndex(lines);
|
|
706
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
707
|
+
const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
708
|
+
for (let i = 0; i < limit; i += 1) {
|
|
709
|
+
const m = pattern.exec(lines[i]!);
|
|
710
|
+
if (m) {
|
|
711
|
+
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
712
|
+
return bom + joinLines(lines, eol);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
lines.splice(limit, 0, `${key} = ${value}`);
|
|
716
|
+
return bom + joinLines(lines, eol);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Set or REMOVE a root-scope basic string. `null` removes the key.
|
|
721
|
+
*
|
|
722
|
+
* Removal is what selecting the default variant does, and it has to be a real deletion
|
|
723
|
+
* rather than an empty string: `model_instructions_file = ""` is a path Codex would try
|
|
724
|
+
* to read, not an absent setting.
|
|
725
|
+
*/
|
|
726
|
+
function setRootString(content: string, key: string, value: string | null): string {
|
|
727
|
+
const eol = dominantEol(content);
|
|
728
|
+
const { bom, body } = splitBom(content);
|
|
729
|
+
const lines = splitLines(body);
|
|
730
|
+
const limit = firstTableIndex(lines);
|
|
731
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
732
|
+
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*"[^"]*"\\s*(?:#.*)?$`);
|
|
733
|
+
for (let i = 0; i < limit; i += 1) {
|
|
734
|
+
if (!pattern.test(lines[i]!)) continue;
|
|
735
|
+
if (value === null) lines.splice(i, 1);
|
|
736
|
+
else lines[i] = `${key} = ${encodeBasicString(value)}`;
|
|
737
|
+
return bom + joinLines(lines, eol);
|
|
738
|
+
}
|
|
739
|
+
if (value === null) return bom + joinLines(lines, eol);
|
|
740
|
+
lines.splice(limit, 0, `${key} = ${encodeBasicString(value)}`);
|
|
741
|
+
return bom + joinLines(lines, eol);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Set a boolean inside `[table]`, appending the table when absent. */
|
|
745
|
+
function setTableBool(content: string, table: string, key: string, value: boolean): string {
|
|
746
|
+
const eol = dominantEol(content);
|
|
747
|
+
const { bom, body } = splitBom(content);
|
|
748
|
+
const lines = splitLines(body);
|
|
749
|
+
const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
750
|
+
const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
|
|
751
|
+
if (start === -1) {
|
|
752
|
+
const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
753
|
+
lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`);
|
|
754
|
+
return bom + joinLines(lines, eol);
|
|
755
|
+
}
|
|
756
|
+
const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
757
|
+
const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
758
|
+
let end = start + 1;
|
|
759
|
+
while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1;
|
|
760
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
761
|
+
const m = pattern.exec(lines[i]!);
|
|
762
|
+
if (m) {
|
|
763
|
+
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
764
|
+
return bom + joinLines(lines, eol);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
lines.splice(end, 0, `${key} = ${value}`);
|
|
768
|
+
return bom + joinLines(lines, eol);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Replace, insert, or remove the generated two-line block. Canonical form is
|
|
773
|
+
* marker + assignment at the top of the document; replacement is "find the
|
|
774
|
+
* marker, replace the next line" rather than a span search.
|
|
775
|
+
*/
|
|
776
|
+
function setProjection(content: string | null, projection: string | null): string {
|
|
777
|
+
const base = content ?? "";
|
|
778
|
+
const eol = dominantEol(base);
|
|
779
|
+
// The BOM is held aside for the whole edit. This is the function that produced
|
|
780
|
+
// the corruption: the insert below is at index 0, which put the marker line
|
|
781
|
+
// ahead of a byte that is only legal at byte 0.
|
|
782
|
+
const { bom, body } = splitBom(base);
|
|
783
|
+
const lines = splitLines(body);
|
|
784
|
+
const limit = firstTableIndex(lines);
|
|
785
|
+
|
|
786
|
+
let markerAt = -1;
|
|
787
|
+
for (let i = 0; i < limit; i += 1) {
|
|
788
|
+
if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER) && ANY_DEV_INSTRUCTIONS.test(lines[i]!)) {
|
|
789
|
+
markerAt = i - 1;
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (markerAt !== -1) {
|
|
795
|
+
if (projection === null) lines.splice(markerAt, 2);
|
|
796
|
+
else lines[markerAt + 1] = `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`;
|
|
797
|
+
return bom + joinLines(lines, eol);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (projection === null) return bom + joinLines(lines, eol);
|
|
801
|
+
lines.splice(0, 0, OCX_SECTION_MARKER, `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`);
|
|
802
|
+
return bom + joinLines(lines, eol);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function serializeStore(layers: readonly CustomLayer[]): string {
|
|
806
|
+
return `${JSON.stringify({ layers }, null, 2)}\n`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
interface Mutation {
|
|
810
|
+
nextConfig: string | null;
|
|
811
|
+
nextStore: string | null;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* The seven-step transaction. Every filesystem mutation in this module goes
|
|
816
|
+
* through here so the journal, the lock, and the per-target byte checks cannot
|
|
817
|
+
* be bypassed by a future caller.
|
|
818
|
+
*/
|
|
819
|
+
function commit(
|
|
820
|
+
opts: Paths | undefined,
|
|
821
|
+
revision: string,
|
|
822
|
+
build: (snapshot: PromptLayerSnapshot, configBytes: string | null, storeBytes: string | null)
|
|
823
|
+
=> Mutation | { error: WriteError; detail?: string },
|
|
824
|
+
): WriteResult {
|
|
825
|
+
const configPath = activeConfigPath(opts);
|
|
826
|
+
const storePath = activeStorePath(opts);
|
|
827
|
+
const journalPath = journalPathFor(storePath);
|
|
828
|
+
const lockPath = lockPathFor(storePath);
|
|
829
|
+
|
|
830
|
+
ensureDir(configPath);
|
|
831
|
+
ensureDir(storePath);
|
|
832
|
+
|
|
833
|
+
const acquired = tryAcquire(lockPath);
|
|
834
|
+
if (!acquired.ok) return { ok: false, error: "locked" };
|
|
835
|
+
const handle = acquired.handle;
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
// 1. recovery first: a journal on disk means an earlier attempt never
|
|
839
|
+
// committed, and we must not stack a second transaction on top of it.
|
|
840
|
+
const recovered = recoverJournal(journalPath, { configPath, storePath });
|
|
841
|
+
if (!recovered.ok) return { ok: false, error: "recovery_required", detail: recovered.detail };
|
|
842
|
+
|
|
843
|
+
// 2. re-read and compare against the caller's edit base.
|
|
844
|
+
const configBytes = readFileOrNull(configPath);
|
|
845
|
+
const storeBytes = readFileOrNull(storePath);
|
|
846
|
+
if (existsSync(configPath) && configBytes === null) {
|
|
847
|
+
return { ok: false, error: "config_unreadable" };
|
|
848
|
+
}
|
|
849
|
+
if (computeRevision(configBytes, storeBytes) !== revision) {
|
|
850
|
+
return { ok: false, error: "stale_revision" };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const snapshot = readPromptLayers({ ...opts, configPath, storePath });
|
|
854
|
+
const built = build(snapshot, configBytes, storeBytes);
|
|
855
|
+
if ("error" in built) return { ok: false, error: built.error, detail: built.detail };
|
|
856
|
+
|
|
857
|
+
const { nextConfig, nextStore } = built;
|
|
858
|
+
const configChanged = nextConfig !== configBytes;
|
|
859
|
+
const storeChanged = nextStore !== storeBytes;
|
|
860
|
+
if (!configChanged && !storeChanged) {
|
|
861
|
+
return { ok: true, changed: false, snapshot };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 3. journal the intent. Its existence is NOT a commit.
|
|
865
|
+
const record: JournalRecord = {
|
|
866
|
+
configPath,
|
|
867
|
+
storePath,
|
|
868
|
+
preConfig: hashBytes(configBytes),
|
|
869
|
+
postConfig: hashBytes(nextConfig),
|
|
870
|
+
preStore: hashBytes(storeBytes),
|
|
871
|
+
postStore: hashBytes(nextStore),
|
|
872
|
+
preConfigBytes: configBytes,
|
|
873
|
+
postConfigBytes: nextConfig,
|
|
874
|
+
preStoreBytes: storeBytes,
|
|
875
|
+
postStoreBytes: nextStore,
|
|
876
|
+
};
|
|
877
|
+
durableWrite(journalPath, encodeJournal(record));
|
|
878
|
+
|
|
879
|
+
// 4/5. each target re-verifies ITS OWN bytes immediately before its rename,
|
|
880
|
+
// so a third party writing between step 2 and here is not overwritten.
|
|
881
|
+
//
|
|
882
|
+
// Wrapped, because a THROW here used to escape the transaction entirely.
|
|
883
|
+
// Only `config` readability is pre-checked, so an unwritable STORE — a
|
|
884
|
+
// directory sitting on its path, a permission change, a full disk — raised
|
|
885
|
+
// out of `durableWrite` after the config had already been renamed into
|
|
886
|
+
// place. The caller saw an exception, the config carried a projection whose
|
|
887
|
+
// store did not exist, and the journal stayed behind claiming an
|
|
888
|
+
// uncommitted intent. Every later write then failed recovery_required.
|
|
889
|
+
//
|
|
890
|
+
// Rolling back on the way out restores the pre-state we recorded and drops
|
|
891
|
+
// the journal, so a failed write leaves the pair exactly as it was found.
|
|
892
|
+
try {
|
|
893
|
+
if (configChanged) {
|
|
894
|
+
if (hashBytes(readFileOrNull(configPath)) !== record.preConfig) {
|
|
895
|
+
return rollback(record, journalPath, "stale_revision");
|
|
896
|
+
}
|
|
897
|
+
if (nextConfig === null) durableDelete(configPath);
|
|
898
|
+
else durableWrite(configPath, nextConfig);
|
|
899
|
+
}
|
|
900
|
+
if (storeChanged) {
|
|
901
|
+
if (hashBytes(readFileOrNull(storePath)) !== record.preStore) {
|
|
902
|
+
return rollback(record, journalPath, "stale_revision");
|
|
903
|
+
}
|
|
904
|
+
if (nextStore === null) durableDelete(storePath);
|
|
905
|
+
else durableWrite(storePath, nextStore);
|
|
906
|
+
}
|
|
907
|
+
} catch (error) {
|
|
908
|
+
// `rollback` is byte-hash driven and refuses to touch a file it does not
|
|
909
|
+
// recognise, so it is safe to run against a partially applied pair. If it
|
|
910
|
+
// cannot account for what it finds it returns recovery_required, which is the
|
|
911
|
+
// honest answer — better than a silent half-write either way.
|
|
912
|
+
const undone = rollback(record, journalPath, "write_failed");
|
|
913
|
+
return { ...undone, detail: error instanceof Error ? error.message : String(error) } as WriteResult;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// 6. verify COMPLETE bytes, not just our two lines: another writer could
|
|
917
|
+
// change an unrelated key and a narrow check would report success.
|
|
918
|
+
const finalConfig = hashBytes(readFileOrNull(configPath));
|
|
919
|
+
const finalStore = hashBytes(readFileOrNull(storePath));
|
|
920
|
+
if (finalConfig !== record.postConfig || finalStore !== record.postStore) {
|
|
921
|
+
return { ok: false, error: "write_superseded" };
|
|
922
|
+
}
|
|
923
|
+
if (!stillHeld(handle)) return { ok: false, error: "write_superseded" };
|
|
924
|
+
|
|
925
|
+
durableDelete(journalPath); // this deletion is the commit
|
|
926
|
+
// The FULL opts, not just the two paths this transaction owns: rebuilding the
|
|
927
|
+
// snapshot from a narrowed object dropped the injected variant directory, so every
|
|
928
|
+
// successful write reported an empty variant list back to its caller.
|
|
929
|
+
return { ok: true, changed: true, snapshot: readPromptLayers({ ...opts, configPath, storePath }) };
|
|
930
|
+
} finally {
|
|
931
|
+
release(handle);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Undo whatever landed, then drop the journal. Never touches an unknown file. */
|
|
936
|
+
function rollback(record: JournalRecord, journalPath: string, error: WriteError): WriteResult {
|
|
937
|
+
const configNow = hashBytes(readFileOrNull(record.configPath));
|
|
938
|
+
const storeNow = hashBytes(readFileOrNull(record.storePath));
|
|
939
|
+
if (configNow !== record.preConfig && configNow !== record.postConfig) {
|
|
940
|
+
return { ok: false, error: "recovery_required", detail: record.configPath };
|
|
941
|
+
}
|
|
942
|
+
if (storeNow !== record.preStore && storeNow !== record.postStore) {
|
|
943
|
+
return { ok: false, error: "recovery_required", detail: record.storePath };
|
|
944
|
+
}
|
|
945
|
+
if (configNow === record.postConfig) {
|
|
946
|
+
if (record.preConfigBytes === null) durableDelete(record.configPath);
|
|
947
|
+
else durableWrite(record.configPath, record.preConfigBytes);
|
|
948
|
+
}
|
|
949
|
+
if (storeNow === record.postStore) {
|
|
950
|
+
if (record.preStoreBytes === null) durableDelete(record.storePath);
|
|
951
|
+
else durableWrite(record.storePath, record.preStoreBytes);
|
|
952
|
+
}
|
|
953
|
+
durableDelete(journalPath);
|
|
954
|
+
return { ok: false, error };
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** Flip one of the five prompt toggles. */
|
|
958
|
+
export function setToggle(id: string, enabled: boolean, revision: string, opts?: Paths): WriteResult {
|
|
959
|
+
if (!isToggleId(id)) return { ok: false, error: "unknown_layer" };
|
|
960
|
+
const spec = TOGGLE_KEYS[id];
|
|
961
|
+
return commit(opts, revision, (_snapshot, configBytes, storeBytes) => ({
|
|
962
|
+
nextConfig: spec.table
|
|
963
|
+
? setTableBool(configBytes ?? "", spec.table, spec.key, enabled)
|
|
964
|
+
: setRootBool(configBytes ?? "", spec.key, enabled),
|
|
965
|
+
nextStore: storeBytes,
|
|
966
|
+
}));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Point `model_instructions_file` at a variant, or remove it for the default.
|
|
971
|
+
*
|
|
972
|
+
* Refusals, each for a reason the GUI cannot be trusted to enforce alone:
|
|
973
|
+
* - an unknown variant id, because the key would name a file Codex cannot read;
|
|
974
|
+
* - the `external` state, because retargeting a key somebody else set silently
|
|
975
|
+
* discards their base prompt. Adopting it is a separate, explicit act.
|
|
976
|
+
*/
|
|
977
|
+
export function selectBaseVariant(selection: BaseSelection, revision: string, opts?: Paths): WriteResult {
|
|
978
|
+
if (selection.kind === "external") return { ok: false, error: "unknown_layer", detail: "cannot select the external state" };
|
|
979
|
+
const dir = activeBaseVariantDir(opts);
|
|
980
|
+
return commit(opts, revision, (snapshot, configBytes, storeBytes) => {
|
|
981
|
+
if (snapshot.baseSelection.kind === "external") {
|
|
982
|
+
return { error: "developer_instructions_not_owned", detail: snapshot.baseSelection.path };
|
|
983
|
+
}
|
|
984
|
+
if (selection.kind === "variant") {
|
|
985
|
+
const variant = snapshot.baseVariants.find(v => v.id === selection.id);
|
|
986
|
+
if (!variant) return { error: "unknown_layer", detail: selection.id };
|
|
987
|
+
}
|
|
988
|
+
const next = selection.kind === "default"
|
|
989
|
+
? null
|
|
990
|
+
: resolve(join(dir, `${selection.id}.md`));
|
|
991
|
+
return {
|
|
992
|
+
nextConfig: setRootString(configBytes ?? "", "model_instructions_file", next),
|
|
993
|
+
nextStore: storeBytes,
|
|
994
|
+
};
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** How many authored variants a user may keep. Two plus the default is the ask. */
|
|
999
|
+
export const MAX_BASE_VARIANTS = 2;
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Write or delete one authored variant body.
|
|
1003
|
+
*
|
|
1004
|
+
* Ordering is deliberate and was learned from a defect in this same module: the FILE is
|
|
1005
|
+
* written and verified before `config.toml` is ever pointed at it. Pointing first would
|
|
1006
|
+
* leave the key naming a file that may not exist, which is a worse failure than a written
|
|
1007
|
+
* file nothing references yet.
|
|
1008
|
+
*
|
|
1009
|
+
* Deleting the variant that is currently SELECTED also clears the key in the same
|
|
1010
|
+
* transaction, so the config can never outlive the file it names.
|
|
1011
|
+
*/
|
|
1012
|
+
export function writeBaseVariant(
|
|
1013
|
+
input: { id: string | null; title: string; body: string } | { id: string; delete: true },
|
|
1014
|
+
revision: string,
|
|
1015
|
+
opts?: Paths,
|
|
1016
|
+
): WriteResult {
|
|
1017
|
+
const dir = activeBaseVariantDir(opts);
|
|
1018
|
+
const deleting = "delete" in input;
|
|
1019
|
+
if (!deleting) {
|
|
1020
|
+
const normalized = normalizeBody(input.body);
|
|
1021
|
+
const invalid = findInvalidCharacter(normalized);
|
|
1022
|
+
if (invalid !== null) {
|
|
1023
|
+
return { ok: false, error: "invalid_characters", detail: `at code point ${invalid.position}` };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const existing = readBaseVariants(opts);
|
|
1027
|
+
const targetId = deleting
|
|
1028
|
+
? input.id
|
|
1029
|
+
: input.id ?? newBaseVariantId(existing);
|
|
1030
|
+
if (!BASE_VARIANT_ID.test(targetId)) return { ok: false, error: "unknown_layer", detail: targetId };
|
|
1031
|
+
if (deleting && !existing.some(v => v.id === targetId)) {
|
|
1032
|
+
return { ok: false, error: "unknown_layer", detail: targetId };
|
|
1033
|
+
}
|
|
1034
|
+
if (!deleting && input.id === null && existing.length >= MAX_BASE_VARIANTS) {
|
|
1035
|
+
return { ok: false, error: "unknown_layer", detail: `at most ${MAX_BASE_VARIANTS} variants` };
|
|
1036
|
+
}
|
|
1037
|
+
const path = join(dir, `${targetId}.md`);
|
|
1038
|
+
const before = readFileOrNull(path);
|
|
1039
|
+
const next = deleting
|
|
1040
|
+
? null
|
|
1041
|
+
: `# ${input.title.replace(/[\r\n]+/g, " ").trim() || targetId}\n${normalizeBody(input.body)}`;
|
|
1042
|
+
|
|
1043
|
+
// Whether this id is the live selection has to be decided while the file still
|
|
1044
|
+
// EXISTS. Deleting first made `resolveBaseSelection` fall through to `external` - the
|
|
1045
|
+
// path no longer matched a known variant - so the config half saw a state it refuses
|
|
1046
|
+
// to touch and left the key pointing at a file that was already gone.
|
|
1047
|
+
const selectedBefore = resolveBaseSelection(readFileOrNull(activeConfigPath(opts)), existing, opts);
|
|
1048
|
+
const clearingKey = deleting
|
|
1049
|
+
&& selectedBefore.kind === "variant"
|
|
1050
|
+
&& selectedBefore.id === targetId;
|
|
1051
|
+
|
|
1052
|
+
// On a CREATE or EDIT the file goes first: pointing config.toml at a file that does
|
|
1053
|
+
// not exist yet is worse than writing a file nothing references. On a DELETE the
|
|
1054
|
+
// order is reversed for the same reason read the other way - the key must stop
|
|
1055
|
+
// naming the file before the file disappears.
|
|
1056
|
+
if (!deleting) {
|
|
1057
|
+
ensureDir(path);
|
|
1058
|
+
try {
|
|
1059
|
+
durableWrite(path, next!);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
return { ok: false, error: "write_failed", detail: error instanceof Error ? error.message : String(error) };
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const result = commit(opts, revision, (_snapshot, configBytes, storeBytes) => ({
|
|
1066
|
+
nextConfig: clearingKey
|
|
1067
|
+
? setRootString(configBytes ?? "", "model_instructions_file", null)
|
|
1068
|
+
: configBytes,
|
|
1069
|
+
nextStore: storeBytes,
|
|
1070
|
+
}));
|
|
1071
|
+
|
|
1072
|
+
if (!result.ok) {
|
|
1073
|
+
// Undo the file half rather than leaving a variant the caller was told was not
|
|
1074
|
+
// written. A delete has not touched the file yet, so there is nothing to undo.
|
|
1075
|
+
if (!deleting) {
|
|
1076
|
+
try {
|
|
1077
|
+
if (before === null) durableDelete(path);
|
|
1078
|
+
else durableWrite(path, before);
|
|
1079
|
+
} catch { /* the returned error already tells the caller to look */ }
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (deleting) {
|
|
1085
|
+
try {
|
|
1086
|
+
durableDelete(path);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
// The key is already clear, so the prompt is correct; the stale file is inert.
|
|
1089
|
+
return { ok: false, error: "write_failed", detail: error instanceof Error ? error.message : String(error) };
|
|
1090
|
+
}
|
|
1091
|
+
// Re-read so the caller sees the variant actually gone.
|
|
1092
|
+
return { ok: true, changed: true, snapshot: readPromptLayers(opts) };
|
|
1093
|
+
}
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function newBaseVariantId(existing: readonly BaseVariant[]): string {
|
|
1098
|
+
const taken = new Set(existing.map(v => v.id));
|
|
1099
|
+
for (;;) {
|
|
1100
|
+
const id = randomBytes(4).toString("hex").slice(0, 6);
|
|
1101
|
+
if (!taken.has(id)) return id;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** Replace the whole custom-layer list; order is composition order. */
|
|
1106
|
+
export function writeCustomLayers(layers: readonly CustomLayer[], revision: string, opts?: Paths): WriteResult {
|
|
1107
|
+
for (const layer of layers) {
|
|
1108
|
+
const normalized = normalizeBody(layer.body);
|
|
1109
|
+
const invalid = findInvalidCharacter(normalized);
|
|
1110
|
+
if (invalid !== null) {
|
|
1111
|
+
return { ok: false, error: "invalid_characters", detail: `layer ${layer.id} at code point ${invalid.position}` };
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const normalizedLayers = layers.map(l => ({ ...l, body: normalizeBody(l.body) }));
|
|
1115
|
+
return commit(opts, revision, (snapshot, configBytes, _storeBytes) => {
|
|
1116
|
+
// Only a marker-owned key may be rewritten. Absent is fine — we create it.
|
|
1117
|
+
const ownership = inspectOwnership(configBytes);
|
|
1118
|
+
if (ownership.state === "external" || ownership.state === "owned-malformed") {
|
|
1119
|
+
return { error: "developer_instructions_not_owned" };
|
|
1120
|
+
}
|
|
1121
|
+
const projection = composeProjection(normalizedLayers);
|
|
1122
|
+
return {
|
|
1123
|
+
nextConfig: setProjection(configBytes, projection.length > 0 ? projection : null),
|
|
1124
|
+
nextStore: serializeStore(normalizedLayers),
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// ---------------------------------------------------------------------------
|
|
1130
|
+
// Adoption — taking ownership of an externally authored key.
|
|
1131
|
+
//
|
|
1132
|
+
// Refusing alone was a dead end: the earlier answer was "delete your existing
|
|
1133
|
+
// instructions by hand", which is not a feature. Adoption previews the raw line
|
|
1134
|
+
// AND the exact body that will be committed, then imports on confirmation.
|
|
1135
|
+
// ---------------------------------------------------------------------------
|
|
1136
|
+
|
|
1137
|
+
export interface AdoptPreview {
|
|
1138
|
+
rawLine: string | null;
|
|
1139
|
+
/** post-normalization body — byte-identical to what a confirm would store */
|
|
1140
|
+
decodedBody: string | null;
|
|
1141
|
+
reason: "ok" | "nothing_to_adopt" | "unsupported_form" | "invalid_characters";
|
|
1142
|
+
path: string;
|
|
1143
|
+
line: number | null;
|
|
1144
|
+
detail?: string;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function newLayerId(existing: readonly CustomLayer[]): string {
|
|
1148
|
+
const taken = new Set(existing.map(l => l.id));
|
|
1149
|
+
for (;;) {
|
|
1150
|
+
const id = randomBytes(4).toString("hex").slice(0, 6);
|
|
1151
|
+
if (!taken.has(id)) return id;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Read-only. Runs the same five steps a confirm would, so preview and commit
|
|
1157
|
+
* cannot disagree: decode -> normalize -> validate -> cap -> present.
|
|
1158
|
+
*/
|
|
1159
|
+
export function previewAdopt(opts?: Paths): AdoptPreview {
|
|
1160
|
+
const configPath = activeConfigPath(opts);
|
|
1161
|
+
const ownership = inspectOwnership(readFileOrNull(configPath));
|
|
1162
|
+
|
|
1163
|
+
if (ownership.state === "absent" || ownership.state === "owned") {
|
|
1164
|
+
return { rawLine: null, decodedBody: null, reason: "nothing_to_adopt", path: configPath, line: null };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const raw = ownership.raw;
|
|
1168
|
+
const line = ownership.line;
|
|
1169
|
+
const eq = raw.indexOf("=");
|
|
1170
|
+
const literal = eq === -1 ? "" : raw.slice(eq + 1).trim().replace(/\s*#.*$/, "");
|
|
1171
|
+
const decoded = decodeBasicString(literal);
|
|
1172
|
+
if (decoded === null) {
|
|
1173
|
+
return {
|
|
1174
|
+
rawLine: raw,
|
|
1175
|
+
decodedBody: null,
|
|
1176
|
+
reason: "unsupported_form",
|
|
1177
|
+
path: configPath,
|
|
1178
|
+
line,
|
|
1179
|
+
detail: "only a single-line basic string using \\\", \\\\ and \\n can be decoded safely",
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const normalized = normalizeBody(decoded);
|
|
1184
|
+
const invalid = findInvalidCharacter(normalized);
|
|
1185
|
+
if (invalid !== null) {
|
|
1186
|
+
return {
|
|
1187
|
+
rawLine: raw,
|
|
1188
|
+
decodedBody: null,
|
|
1189
|
+
reason: "invalid_characters",
|
|
1190
|
+
path: configPath,
|
|
1191
|
+
line,
|
|
1192
|
+
detail: `code point ${invalid.position} is a ${invalid.reason}`,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return { rawLine: raw, decodedBody: normalized, reason: "ok", path: configPath, line };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** Import the external value as one custom layer and take ownership of the key. */
|
|
1200
|
+
export function adoptDeveloperInstructions(revision: string, opts?: Paths): WriteResult {
|
|
1201
|
+
const preview = previewAdopt(opts);
|
|
1202
|
+
if (preview.reason !== "ok" || preview.decodedBody === null) {
|
|
1203
|
+
return {
|
|
1204
|
+
ok: false,
|
|
1205
|
+
error: preview.reason === "invalid_characters" ? "invalid_characters" : "developer_instructions_not_owned",
|
|
1206
|
+
detail: preview.detail,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
const body = preview.decodedBody;
|
|
1210
|
+
return commit(opts, revision, (snapshot, configBytes) => {
|
|
1211
|
+
const existing = snapshot.custom;
|
|
1212
|
+
const adopted: CustomLayer = {
|
|
1213
|
+
id: newLayerId(existing),
|
|
1214
|
+
title: "Imported from config.toml",
|
|
1215
|
+
body,
|
|
1216
|
+
enabled: true,
|
|
1217
|
+
};
|
|
1218
|
+
const layers = [adopted, ...existing];
|
|
1219
|
+
// Drop the unowned line first, then write the canonical owned block.
|
|
1220
|
+
const stripped = removeUnownedProjection(configBytes ?? "");
|
|
1221
|
+
return {
|
|
1222
|
+
nextConfig: setProjection(stripped, composeProjection(layers)),
|
|
1223
|
+
nextStore: serializeStore(layers),
|
|
1224
|
+
};
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** Remove an unowned or reshaped `developer_instructions` from the root scope. */
|
|
1229
|
+
function removeUnownedProjection(content: string): string {
|
|
1230
|
+
const eol = dominantEol(content);
|
|
1231
|
+
const lines = splitLines(content);
|
|
1232
|
+
const limit = firstTableIndex(lines);
|
|
1233
|
+
for (let i = 0; i < limit; i += 1) {
|
|
1234
|
+
if (!ANY_DEV_INSTRUCTIONS.test(lines[i]!)) continue;
|
|
1235
|
+
const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
|
|
1236
|
+
lines.splice(marked ? i - 1 : i, marked ? 2 : 1);
|
|
1237
|
+
return joinLines(lines, eol);
|
|
1238
|
+
}
|
|
1239
|
+
return joinLines(lines, eol);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// ---------------------------------------------------------------------------
|
|
1243
|
+
// Salvage — the store is gone while a live projection remains.
|
|
1244
|
+
//
|
|
1245
|
+
// This is salvage, NOT reconstruction. The projection is one concatenated
|
|
1246
|
+
// string: layer boundaries, ids, titles, order, disabled layers, and whether a
|
|
1247
|
+
// blank line separated two layers or was the user's own text are all gone.
|
|
1248
|
+
// ---------------------------------------------------------------------------
|
|
1249
|
+
|
|
1250
|
+
export interface SalvagePreview {
|
|
1251
|
+
body: string | null;
|
|
1252
|
+
/** the DIRECTORY backups land in. A read-only preview reserves no filename. */
|
|
1253
|
+
backupDir: string;
|
|
1254
|
+
unrecoverable: readonly string[];
|
|
1255
|
+
reason: "ok" | "nothing_to_salvage";
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const UNRECOVERABLE = Object.freeze([
|
|
1259
|
+
"layer boundaries",
|
|
1260
|
+
"layer ids",
|
|
1261
|
+
"layer titles",
|
|
1262
|
+
"row order",
|
|
1263
|
+
"disabled layers and their bodies",
|
|
1264
|
+
"whether a blank line separated two layers or was your own text",
|
|
1265
|
+
]);
|
|
1266
|
+
|
|
1267
|
+
export function previewSalvage(opts?: Paths): SalvagePreview {
|
|
1268
|
+
const configPath = activeConfigPath(opts);
|
|
1269
|
+
const storePath = activeStorePath(opts);
|
|
1270
|
+
const ownership = inspectOwnership(readFileOrNull(configPath));
|
|
1271
|
+
const body = ownership.state === "owned" ? decodeBasicString(ownership.literal) : null;
|
|
1272
|
+
return {
|
|
1273
|
+
body,
|
|
1274
|
+
// `dirname`, not a hand-rolled `lastIndexOf("/")`: a Windows store path is
|
|
1275
|
+
// `D:\...\store.toml`, which contains no forward slash at all, so the
|
|
1276
|
+
// slice returned `"."` and the preview named the wrong directory.
|
|
1277
|
+
backupDir: dirname(storePath),
|
|
1278
|
+
unrecoverable: UNRECOVERABLE,
|
|
1279
|
+
reason: body !== null && body.length > 0 ? "ok" : "nothing_to_salvage",
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Adopt the live projection as ONE layer. A durable backup is written first and
|
|
1285
|
+
* salvage aborts if it cannot be created — a destructive operation whose safety
|
|
1286
|
+
* net failed should not proceed.
|
|
1287
|
+
*/
|
|
1288
|
+
export function salvageProjection(revision: string, opts?: Paths): WriteResult {
|
|
1289
|
+
const preview = previewSalvage(opts);
|
|
1290
|
+
if (preview.reason !== "ok" || preview.body === null) {
|
|
1291
|
+
return { ok: false, error: "developer_instructions_not_owned", detail: "no live projection to salvage" };
|
|
1292
|
+
}
|
|
1293
|
+
const body = preview.body;
|
|
1294
|
+
const storePath = activeStorePath(opts);
|
|
1295
|
+
const backupPath = `${storePath.replace(/\.json$/, "")}.salvage-${Date.now()}-${randomBytes(3).toString("hex")}.txt`;
|
|
1296
|
+
try {
|
|
1297
|
+
durableWriteExclusive(backupPath, body);
|
|
1298
|
+
} catch {
|
|
1299
|
+
return { ok: false, error: "recovery_required", detail: `could not write a durable backup at ${backupPath}` };
|
|
1300
|
+
}
|
|
1301
|
+
return commit(opts, revision, (_snapshot, configBytes) => {
|
|
1302
|
+
const salvaged: CustomLayer = {
|
|
1303
|
+
id: newLayerId([]),
|
|
1304
|
+
title: "Salvaged from config.toml",
|
|
1305
|
+
body,
|
|
1306
|
+
enabled: true,
|
|
1307
|
+
};
|
|
1308
|
+
return {
|
|
1309
|
+
nextConfig: setProjection(configBytes, body),
|
|
1310
|
+
nextStore: serializeStore([salvaged]),
|
|
1311
|
+
};
|
|
1312
|
+
});
|
|
1313
|
+
}
|