@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,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-lock.ts — advisory cross-process lock for prompt-layer mutations.
|
|
3
|
+
*
|
|
4
|
+
* An in-process mutex only serialises browser tabs behind one service. A CLI
|
|
5
|
+
* invocation, a second service, or a stale process can all reach the same
|
|
6
|
+
* files, so the lock lives on disk.
|
|
7
|
+
*
|
|
8
|
+
* STALE TAKEOVER IS A RENAME, NOT AN UNLINK. Naive breaking is racy: A judges
|
|
9
|
+
* the lock stale, B removes it and acquires its own, then A unlinks *B's live
|
|
10
|
+
* lock* and both proceed. Unlinking a path you did not verify is the bug. Here
|
|
11
|
+
* the contender renames the observed stale lock to a token-quarantined name —
|
|
12
|
+
* an atomic operation exactly one contender can win — and only the winner
|
|
13
|
+
* creates the real lock.
|
|
14
|
+
*
|
|
15
|
+
* RELEASE ONLY DELETES A LOCK WHOSE TOKEN IS STILL OURS. A mismatch means we
|
|
16
|
+
* were superseded, and deleting it would hand the critical section to two
|
|
17
|
+
* writers at once.
|
|
18
|
+
*
|
|
19
|
+
* This does NOT cover Codex, which knows nothing about our lock. That residual
|
|
20
|
+
* is handled by the per-target byte checks in the write path, and the rename
|
|
21
|
+
* window itself is documented as irreducible from user space.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
25
|
+
|
|
26
|
+
const FILE_MODE = 0o600;
|
|
27
|
+
|
|
28
|
+
/** A lock younger than this is respected even if its pid looks gone. */
|
|
29
|
+
export const STALE_AFTER_MS = 10_000;
|
|
30
|
+
|
|
31
|
+
export interface LockRecord {
|
|
32
|
+
token: string;
|
|
33
|
+
pid: number;
|
|
34
|
+
acquiredAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LockHandle {
|
|
38
|
+
path: string;
|
|
39
|
+
token: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AcquireResult =
|
|
43
|
+
| { ok: true; handle: LockHandle }
|
|
44
|
+
| { ok: false; error: "locked" };
|
|
45
|
+
|
|
46
|
+
export interface LockDeps {
|
|
47
|
+
/** Injectable so tests can simulate a live or dead owner. */
|
|
48
|
+
isProcessAlive: (pid: number) => boolean;
|
|
49
|
+
now: () => number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaultDeps: LockDeps = {
|
|
53
|
+
isProcessAlive: pid => {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// EPERM means it exists but belongs to another user.
|
|
59
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
now: () => Date.now(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function readRecord(path: string): LockRecord | null {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as LockRecord;
|
|
68
|
+
if (typeof parsed?.token !== "string" || typeof parsed?.pid !== "number") return null;
|
|
69
|
+
return parsed;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True when the holder is gone AND the lock is older than the grace window. */
|
|
76
|
+
function isStale(record: LockRecord | null, deps: LockDeps): boolean {
|
|
77
|
+
if (record === null) return true; // unparseable: treat as debris
|
|
78
|
+
if (deps.isProcessAlive(record.pid)) return false;
|
|
79
|
+
return deps.now() - record.acquiredAt > STALE_AFTER_MS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One attempt. The caller decides whether to retry — a loser must go back to
|
|
84
|
+
* re-reading the lock rather than assuming its quarantine still applies.
|
|
85
|
+
*/
|
|
86
|
+
export function tryAcquire(path: string, deps: LockDeps = defaultDeps): AcquireResult {
|
|
87
|
+
const token = randomBytes(8).toString("hex");
|
|
88
|
+
const record: LockRecord = { token, pid: process.pid, acquiredAt: deps.now() };
|
|
89
|
+
const body = JSON.stringify(record);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(path, body, { encoding: "utf8", mode: FILE_MODE, flag: "wx" });
|
|
93
|
+
return { ok: true, handle: { path, token } };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!isStale(readRecord(path), deps)) return { ok: false, error: "locked" };
|
|
99
|
+
|
|
100
|
+
// Quarantine by rename: atomic, and exactly one contender wins it.
|
|
101
|
+
const quarantine = `${path}.stale-${token}`;
|
|
102
|
+
try {
|
|
103
|
+
renameSync(path, quarantine);
|
|
104
|
+
} catch {
|
|
105
|
+
// Someone else won the rename, or the owner released between our checks.
|
|
106
|
+
// Either way we do NOT touch the path — retry from the top.
|
|
107
|
+
return { ok: false, error: "locked" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(path, body, { encoding: "utf8", mode: FILE_MODE, flag: "wx" });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// A successor acquired the real lock between our rename and this create.
|
|
114
|
+
// Its lock is live and is not ours to remove.
|
|
115
|
+
try { unlinkSync(quarantine); } catch { /* debris */ }
|
|
116
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return { ok: false, error: "locked" };
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try { unlinkSync(quarantine); } catch { /* debris */ }
|
|
121
|
+
return { ok: true, handle: { path, token } };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Release. Deletes nothing unless the on-disk token is still ours; returns false
|
|
126
|
+
* when we were superseded, which the caller surfaces as `write_superseded`.
|
|
127
|
+
*/
|
|
128
|
+
export function release(handle: LockHandle): boolean {
|
|
129
|
+
const record = readRecord(handle.path);
|
|
130
|
+
if (record === null || record.token !== handle.token) return false;
|
|
131
|
+
try {
|
|
132
|
+
unlinkSync(handle.path);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** True when the on-disk lock is still the one this handle acquired. */
|
|
140
|
+
export function stillHeld(handle: LockHandle): boolean {
|
|
141
|
+
if (!existsSync(handle.path)) return false;
|
|
142
|
+
return readRecord(handle.path)?.token === handle.token;
|
|
143
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the prompt text Codex actually assembles.
|
|
3
|
+
*
|
|
4
|
+
* The read-only dialog used to say Codex "does not expose" a layer's text. That
|
|
5
|
+
* was wrong: Codex is open source and ships `codex debug prompt-input`, which
|
|
6
|
+
* renders the model-visible input list as JSON. Reading it is the difference
|
|
7
|
+
* between describing a layer and showing it.
|
|
8
|
+
*
|
|
9
|
+
* What this does NOT cover, stated rather than implied:
|
|
10
|
+
*
|
|
11
|
+
* - `base-instructions` is absent. `prompt_debug.rs` returns `prompt.input` and
|
|
12
|
+
* discards `base_instructions`, so the base prompt never appears here.
|
|
13
|
+
* - World-state sections are DIFF-rendered (`add_section` registers state, it does
|
|
14
|
+
* not emit text). A section that renders nothing on a first turn is missing
|
|
15
|
+
* from this output even though its layer exists.
|
|
16
|
+
* - The output reflects the invoking directory and the current config, not a
|
|
17
|
+
* universal prompt.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { existsSync, statSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { resolveCodexHomeDir } from "./home";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Layer id -> the tag Codex actually renders it under.
|
|
27
|
+
*
|
|
28
|
+
* Every entry here was read off live `codex debug prompt-input` output across the
|
|
29
|
+
* feature flags that enable each section. None is inferred from the Rust section
|
|
30
|
+
* `ID` constants: those are identifiers for diffing, not wrapper tags, and an
|
|
31
|
+
* earlier guess from them mapped `permissions` to the wrong name while the real
|
|
32
|
+
* tag - `permissions instructions`, with a space - went unmatched.
|
|
33
|
+
*
|
|
34
|
+
* A layer absent from this map is reported as unsupported, never as silent.
|
|
35
|
+
*/
|
|
36
|
+
const LAYER_SECTION_TAGS: Record<string, string> = {
|
|
37
|
+
skills: "skills_instructions",
|
|
38
|
+
apps: "apps_instructions",
|
|
39
|
+
plugins: "plugins_instructions",
|
|
40
|
+
environment: "environment_context",
|
|
41
|
+
permissions: "permissions instructions",
|
|
42
|
+
// Synthetic: the project doc carries no tag of its own (see extractSections).
|
|
43
|
+
"agents-md": "__agents_md",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Inventory ids with no confirmed tag in the rendered output. They are listed
|
|
48
|
+
* explicitly rather than left missing: an absent entry made the dialog report a
|
|
49
|
+
* successful probe as unavailable.
|
|
50
|
+
*/
|
|
51
|
+
const UNMAPPED_LAYER_IDS = [
|
|
52
|
+
"model-switch",
|
|
53
|
+
"context-window-guidance",
|
|
54
|
+
"environments-instructions",
|
|
55
|
+
"tools",
|
|
56
|
+
"multi-agent-mode",
|
|
57
|
+
// Confirmed absent from live output under their own feature flags, so the
|
|
58
|
+
// previous `personality` / `realtime` guesses reported these as silent when the
|
|
59
|
+
// truth is that this extractor has no verified tag for them.
|
|
60
|
+
"personality",
|
|
61
|
+
"realtime",
|
|
62
|
+
"collaboration",
|
|
63
|
+
// The Rust source names a <git_attribution> marker pair, but a world-state section is
|
|
64
|
+
// DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
|
|
65
|
+
// `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
|
|
66
|
+
// no attribution text. Listing the id here reports "not exposed" honestly instead of
|
|
67
|
+
// claiming a tag this extractor has never actually matched - the same mistake the
|
|
68
|
+
// header above records for permissions.
|
|
69
|
+
"git-attribution",
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
72
|
+
export interface LayerText {
|
|
73
|
+
/** Rendered text, when this layer produced a section on the probed turn. */
|
|
74
|
+
text: string | null;
|
|
75
|
+
/** Why the text is absent, when it is. */
|
|
76
|
+
reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unavailable";
|
|
77
|
+
bytes: number;
|
|
78
|
+
/** For `empty-source`: the file that exists but has nothing in it. */
|
|
79
|
+
sourcePath?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PromptTextProbe {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
/** The Codex home the probe reported on. */
|
|
85
|
+
codexHome: string;
|
|
86
|
+
layers: Record<string, LayerText>;
|
|
87
|
+
detail?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveCodexBinary(): string | null {
|
|
91
|
+
const candidates = [
|
|
92
|
+
join(homedir(), ".codex/packages/standalone/current/bin/codex"),
|
|
93
|
+
join(homedir(), ".local/bin/codex"),
|
|
94
|
+
"/usr/local/bin/codex",
|
|
95
|
+
"/opt/homebrew/bin/codex",
|
|
96
|
+
];
|
|
97
|
+
return candidates.find(path => existsSync(path)) ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 8 MiB is far above any real prompt and far below anything that hurts the server. */
|
|
101
|
+
const MAX_PROBE_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
102
|
+
|
|
103
|
+
function runProbe(binary: string, cwd: string, timeoutMs: number): Promise<string | null> {
|
|
104
|
+
return new Promise(resolve => {
|
|
105
|
+
// A probe must never hang OR balloon the management API: it is bounded in
|
|
106
|
+
// time AND in bytes, and every failure degrades to "unavailable" rather than
|
|
107
|
+
// an error page.
|
|
108
|
+
const child = spawn(binary, ["debug", "prompt-input"], {
|
|
109
|
+
cwd,
|
|
110
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
111
|
+
});
|
|
112
|
+
const chunks: Buffer[] = [];
|
|
113
|
+
let size = 0;
|
|
114
|
+
let settled = false;
|
|
115
|
+
// One settlement path: a timeout that resolved before `close` used to leave
|
|
116
|
+
// the child streaming into a buffer nobody would ever read.
|
|
117
|
+
const settle = (value: string | null) => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
settled = true;
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
child.stdout?.destroy();
|
|
122
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
123
|
+
resolve(value);
|
|
124
|
+
};
|
|
125
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
126
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
127
|
+
size += chunk.length;
|
|
128
|
+
if (size > MAX_PROBE_OUTPUT_BYTES) { settle(null); return; }
|
|
129
|
+
chunks.push(chunk);
|
|
130
|
+
});
|
|
131
|
+
child.on("error", () => settle(null));
|
|
132
|
+
child.on("close", code => {
|
|
133
|
+
// Decode once, at the end: `String(chunk)` per chunk corrupts any UTF-8
|
|
134
|
+
// character that straddles a chunk boundary.
|
|
135
|
+
settle(code === 0 ? Buffer.concat(chunks).toString("utf8") : null);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Pull every `<tag>...</tag>` section out of the rendered developer messages. */
|
|
141
|
+
function extractSections(raw: string): Map<string, string> {
|
|
142
|
+
const sections = new Map<string, string>();
|
|
143
|
+
let parsed: unknown;
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(raw);
|
|
146
|
+
} catch {
|
|
147
|
+
return sections;
|
|
148
|
+
}
|
|
149
|
+
if (!Array.isArray(parsed)) return sections;
|
|
150
|
+
for (const item of parsed) {
|
|
151
|
+
const content = (item as { content?: unknown }).content;
|
|
152
|
+
if (!Array.isArray(content)) continue;
|
|
153
|
+
const text = content.map(part => String((part as { text?: unknown }).text ?? "")).join("");
|
|
154
|
+
// Tag names are NOT all snake_case: Codex renders `<permissions instructions>`
|
|
155
|
+
// with a space. A `[a-z_]+` pattern silently skipped it, and the layer was
|
|
156
|
+
// reported as "sent nothing" while its text sat right there.
|
|
157
|
+
for (const match of text.matchAll(/<([a-zA-Z_][a-zA-Z0-9_ -]*)>([\s\S]*?)<\/\1>/g)) {
|
|
158
|
+
sections.set(match[1]!, match[2]!.trim());
|
|
159
|
+
}
|
|
160
|
+
// AGENTS.md is NOT tagged: it arrives as a plain `# AGENTS.md instructions
|
|
161
|
+
// for <path>` block among the tagged sections. Matching only on tags would
|
|
162
|
+
// report the layer as unrendered while its text sits in the same message.
|
|
163
|
+
//
|
|
164
|
+
// Bounded at both ends. Capturing to end-of-message swept up any unrelated
|
|
165
|
+
// untagged prose that happened to follow, and stripping tag-shaped blocks
|
|
166
|
+
// first also deleted XML-like text the user had written INSIDE their own
|
|
167
|
+
// AGENTS.md. Codex wraps the body in <INSTRUCTIONS>, so that is the boundary.
|
|
168
|
+
// No line anchor: the block is concatenated directly onto the previous
|
|
169
|
+
// section's closing tag, so requiring a newline before it never matched.
|
|
170
|
+
const projectDoc = /# AGENTS\.md instructions for [^\n]*\n+<INSTRUCTIONS>\n?([\s\S]*?)\n?<\/INSTRUCTIONS>/.exec(text);
|
|
171
|
+
if (projectDoc) sections.set("__agents_md", projectDoc[1]!.trim());
|
|
172
|
+
}
|
|
173
|
+
return sections;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Test seam: the extraction is the part worth pinning, not the spawn. */
|
|
177
|
+
export const extractSectionsForTests = extractSections;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Probe once and map every known layer to its rendered text.
|
|
181
|
+
*
|
|
182
|
+
* `cwd` matters: AGENTS.md and environment context are directory-dependent, so a
|
|
183
|
+
* probe from the wrong place would describe a prompt the user never sees.
|
|
184
|
+
*/
|
|
185
|
+
export async function probePromptText(timeoutMs = 15_000): Promise<PromptTextProbe> {
|
|
186
|
+
// The probe runs in CODEX_HOME, never in a caller-supplied directory. A `cwd`
|
|
187
|
+
// parameter let an authenticated request read any readable folder's AGENTS.md,
|
|
188
|
+
// and it also described a prompt that depends on where Codex happened to run.
|
|
189
|
+
// The global home is the one context this page can honestly report on.
|
|
190
|
+
const codexHome = resolveCodexHomeDir();
|
|
191
|
+
const binary = resolveCodexBinary();
|
|
192
|
+
if (!binary) {
|
|
193
|
+
return { ok: false, codexHome, layers: {}, detail: "codex binary not found" };
|
|
194
|
+
}
|
|
195
|
+
const raw = await runProbe(binary, codexHome, timeoutMs);
|
|
196
|
+
if (raw === null) {
|
|
197
|
+
return { ok: false, codexHome, layers: {}, detail: "codex debug prompt-input failed" };
|
|
198
|
+
}
|
|
199
|
+
const sections = extractSections(raw);
|
|
200
|
+
if (sections.size === 0) {
|
|
201
|
+
// Zero sections from a zero-exit probe means the output did not parse, which
|
|
202
|
+
// is a failed read - not fifteen layers that each chose to send nothing.
|
|
203
|
+
return { ok: false, codexHome, layers: {}, detail: "prompt output could not be parsed" };
|
|
204
|
+
}
|
|
205
|
+
const layers: Record<string, LayerText> = {};
|
|
206
|
+
for (const [layerId, tag] of Object.entries(LAYER_SECTION_TAGS)) {
|
|
207
|
+
const text = sections.get(tag) ?? null;
|
|
208
|
+
layers[layerId] = text === null
|
|
209
|
+
// Registered but not rendered on this turn, which is an ordinary state for
|
|
210
|
+
// a diff-rendered section rather than an error.
|
|
211
|
+
? { text: null, reason: "not-rendered", bytes: 0 }
|
|
212
|
+
: { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8") };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// A file that exists and is empty is not the same as a layer that chose to send
|
|
216
|
+
// nothing. Reporting "sent nothing" for an empty AGENTS.md tells the user their
|
|
217
|
+
// layer is idle when the real answer is that the file they wrote is blank.
|
|
218
|
+
const agentsMdPath = join(codexHome, "AGENTS.md");
|
|
219
|
+
if (layers["agents-md"]?.reason === "not-rendered" && existsSync(agentsMdPath)) {
|
|
220
|
+
try {
|
|
221
|
+
if (statSync(agentsMdPath).size === 0) {
|
|
222
|
+
layers["agents-md"] = { text: null, reason: "empty-source", bytes: 0, sourcePath: agentsMdPath };
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// An unreadable file stays "not-rendered": we cannot claim it is empty.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// The base prompt travels outside prompt.input and cannot be read this way.
|
|
229
|
+
layers["base-instructions"] = { text: null, reason: "not-exposed", bytes: 0 };
|
|
230
|
+
|
|
231
|
+
// Layers whose rendered tag we have not confirmed against live output. Leaving
|
|
232
|
+
// them absent made the GUI fall through to "unavailable", which claims the probe
|
|
233
|
+
// failed when it succeeded. Saying we have no mapping is the smaller claim.
|
|
234
|
+
for (const id of UNMAPPED_LAYER_IDS) {
|
|
235
|
+
layers[id] ??= { text: null, reason: "not-exposed", bytes: 0 };
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, codexHome, layers };
|
|
238
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
2
|
+
|
|
3
|
+
const RESET_ELIGIBLE_CODE_VALUES = [
|
|
4
|
+
"usage_limit_exceeded",
|
|
5
|
+
"insufficient_quota",
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
export type CodexResetEligibleExhaustionCode =
|
|
9
|
+
(typeof RESET_ELIGIBLE_CODE_VALUES)[number];
|
|
10
|
+
|
|
11
|
+
export type CodexPreStreamRejectionKind =
|
|
12
|
+
| "reset-eligible-exhaustion"
|
|
13
|
+
| "generic-rate-limit"
|
|
14
|
+
| "unverified-billing-or-quota"
|
|
15
|
+
| "transient-server-error"
|
|
16
|
+
| "authentication-error"
|
|
17
|
+
| "permission-error"
|
|
18
|
+
| "other";
|
|
19
|
+
|
|
20
|
+
export interface CodexPreStreamRejection {
|
|
21
|
+
kind: CodexPreStreamRejectionKind;
|
|
22
|
+
status: number;
|
|
23
|
+
alternateRetryEligible: boolean;
|
|
24
|
+
resetCreditEligible: boolean;
|
|
25
|
+
semanticCode?: CodexResetEligibleExhaustionCode;
|
|
26
|
+
/**
|
|
27
|
+
* Structured denial evidence for a 403. Present only when the upstream body names a
|
|
28
|
+
* workspace/entitlement denial, which proves the CREDENTIAL is valid and the account
|
|
29
|
+
* simply lacks access here (#1789). Status alone can never set this.
|
|
30
|
+
*/
|
|
31
|
+
denial?: "workspace" | "entitlement";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Upstream codes that identify a WORKSPACE denial rather than a bad credential.
|
|
36
|
+
*
|
|
37
|
+
* #1789: a K12 account whose credential validates and whose WHAM usage returns 200 still
|
|
38
|
+
* gets 403 `codex_workspace_access_denied` on a routed prompt. Treating that as a credential
|
|
39
|
+
* failure tells the user to re-authenticate a credential that is already valid, and the loop
|
|
40
|
+
* repeats forever.
|
|
41
|
+
*/
|
|
42
|
+
const WORKSPACE_DENIAL_CODES: ReadonlySet<string> = new Set([
|
|
43
|
+
"codex_workspace_access_denied",
|
|
44
|
+
"workspace_access_denied",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const ENTITLEMENT_DENIAL_CODES: ReadonlySet<string> = new Set([
|
|
48
|
+
"codex_entitlement_missing",
|
|
49
|
+
"entitlement_missing",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/** Read a structured denial code out of a 403 body. Fails closed to undefined. */
|
|
53
|
+
async function denialFromResponse(
|
|
54
|
+
response: Response,
|
|
55
|
+
signal?: AbortSignal,
|
|
56
|
+
): Promise<"workspace" | "entitlement" | undefined> {
|
|
57
|
+
try {
|
|
58
|
+
const body = await readBoundedResponseBody(response.clone(), { signal, fatalUtf8: true });
|
|
59
|
+
if (!body.displaySafe || body.truncated || !body.text.trim()) return undefined;
|
|
60
|
+
if (isUnsafeJsonDocument(body.text)) return undefined;
|
|
61
|
+
const payload = JSON.parse(body.text) as unknown;
|
|
62
|
+
const code = structuredDenialCode(payload);
|
|
63
|
+
if (code === undefined) return undefined;
|
|
64
|
+
if (WORKSPACE_DENIAL_CODES.has(code)) return "workspace";
|
|
65
|
+
if (ENTITLEMENT_DENIAL_CODES.has(code)) return "entitlement";
|
|
66
|
+
return undefined;
|
|
67
|
+
} catch {
|
|
68
|
+
// Same fail-closed rule as the exhaustion classifier: an unreadable body must not
|
|
69
|
+
// downgrade a credential failure into a workspace one.
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ownStringField(container: Record<string, unknown>, field: string): string | undefined {
|
|
75
|
+
const descriptor = Object.getOwnPropertyDescriptor(container, field);
|
|
76
|
+
return descriptor && "value" in descriptor && typeof descriptor.value === "string"
|
|
77
|
+
? descriptor.value
|
|
78
|
+
: undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Own-property `code` lookup at the top level or under `error` / `detail`. No coercion or accessors. */
|
|
82
|
+
function structuredDenialCode(payload: unknown): string | undefined {
|
|
83
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
84
|
+
const record = payload as Record<string, unknown>;
|
|
85
|
+
const direct = ownStringField(record, "code");
|
|
86
|
+
if (direct !== undefined) return direct;
|
|
87
|
+
|
|
88
|
+
for (const field of ["error", "detail"] as const) {
|
|
89
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, field);
|
|
90
|
+
if (!descriptor || !("value" in descriptor)) continue;
|
|
91
|
+
const nested = descriptor.value;
|
|
92
|
+
if (nested === null || typeof nested !== "object" || Array.isArray(nested)) continue;
|
|
93
|
+
const code = ownStringField(nested as Record<string, unknown>, "code");
|
|
94
|
+
if (code !== undefined) return code;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const RESET_ELIGIBLE_CODES: ReadonlySet<string> = new Set(RESET_ELIGIBLE_CODE_VALUES);
|
|
100
|
+
|
|
101
|
+
const TRANSIENT_SERVER_STATUSES = new Set([500, 502, 503, 504, 520, 521, 522]);
|
|
102
|
+
const JSON_NUMBER_PATTERN = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
103
|
+
|
|
104
|
+
function rejection(
|
|
105
|
+
status: number,
|
|
106
|
+
kind: CodexPreStreamRejectionKind,
|
|
107
|
+
options: {
|
|
108
|
+
alternateRetryEligible?: boolean;
|
|
109
|
+
semanticCode?: CodexResetEligibleExhaustionCode;
|
|
110
|
+
} = {},
|
|
111
|
+
): CodexPreStreamRejection {
|
|
112
|
+
return {
|
|
113
|
+
kind,
|
|
114
|
+
status,
|
|
115
|
+
alternateRetryEligible: options.alternateRetryEligible === true,
|
|
116
|
+
resetCreditEligible: options.semanticCode !== undefined,
|
|
117
|
+
...(options.semanticCode ? { semanticCode: options.semanticCode } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hasOwnField(container: Record<string, unknown>, field: string): boolean {
|
|
122
|
+
return Object.prototype.hasOwnProperty.call(container, field);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type JsonScanResult = {
|
|
126
|
+
next: number;
|
|
127
|
+
duplicate: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function skipJsonWhitespace(text: string, index: number): number {
|
|
131
|
+
while (index < text.length && /[\t\n\r ]/.test(text[index] ?? "")) index += 1;
|
|
132
|
+
return index;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function scanJsonStringEnd(text: string, index: number): number {
|
|
136
|
+
if (text[index] !== '"') throw new SyntaxError("expected JSON string");
|
|
137
|
+
for (let cursor = index + 1; cursor < text.length; cursor += 1) {
|
|
138
|
+
const char = text[cursor];
|
|
139
|
+
if (char === '"') return cursor + 1;
|
|
140
|
+
if (char === "\\") cursor += 1;
|
|
141
|
+
}
|
|
142
|
+
throw new SyntaxError("unterminated JSON string");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scanJsonValue(text: string, index: number): JsonScanResult {
|
|
146
|
+
const start = skipJsonWhitespace(text, index);
|
|
147
|
+
if (text[start] === "{") return scanJsonObject(text, start);
|
|
148
|
+
if (text[start] === "[") return scanJsonArray(text, start);
|
|
149
|
+
if (text[start] === '"') return { next: scanJsonStringEnd(text, start), duplicate: false };
|
|
150
|
+
|
|
151
|
+
for (const literal of ["true", "false", "null"]) {
|
|
152
|
+
if (text.startsWith(literal, start)) {
|
|
153
|
+
return { next: start + literal.length, duplicate: false };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
JSON_NUMBER_PATTERN.lastIndex = start;
|
|
157
|
+
const number = JSON_NUMBER_PATTERN.exec(text);
|
|
158
|
+
if (!number) throw new SyntaxError("expected JSON value");
|
|
159
|
+
return { next: start + number[0].length, duplicate: false };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function scanJsonObject(text: string, index: number): JsonScanResult {
|
|
163
|
+
const keys = new Set<string>();
|
|
164
|
+
let duplicate = false;
|
|
165
|
+
let cursor = skipJsonWhitespace(text, index + 1);
|
|
166
|
+
if (text[cursor] === "}") return { next: cursor + 1, duplicate: false };
|
|
167
|
+
|
|
168
|
+
while (cursor < text.length) {
|
|
169
|
+
const keyEnd = scanJsonStringEnd(text, cursor);
|
|
170
|
+
const key = JSON.parse(text.slice(cursor, keyEnd)) as unknown;
|
|
171
|
+
if (typeof key !== "string") throw new SyntaxError("invalid JSON object key");
|
|
172
|
+
if (keys.has(key)) duplicate = true;
|
|
173
|
+
keys.add(key);
|
|
174
|
+
|
|
175
|
+
cursor = skipJsonWhitespace(text, keyEnd);
|
|
176
|
+
if (text[cursor] !== ":") throw new SyntaxError("expected JSON object colon");
|
|
177
|
+
const value = scanJsonValue(text, cursor + 1);
|
|
178
|
+
duplicate ||= value.duplicate;
|
|
179
|
+
cursor = skipJsonWhitespace(text, value.next);
|
|
180
|
+
if (text[cursor] === "}") return { next: cursor + 1, duplicate };
|
|
181
|
+
if (text[cursor] !== ",") throw new SyntaxError("expected JSON object separator");
|
|
182
|
+
cursor = skipJsonWhitespace(text, cursor + 1);
|
|
183
|
+
}
|
|
184
|
+
throw new SyntaxError("unterminated JSON object");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function scanJsonArray(text: string, index: number): JsonScanResult {
|
|
188
|
+
let duplicate = false;
|
|
189
|
+
let cursor = skipJsonWhitespace(text, index + 1);
|
|
190
|
+
if (text[cursor] === "]") return { next: cursor + 1, duplicate: false };
|
|
191
|
+
|
|
192
|
+
while (cursor < text.length) {
|
|
193
|
+
const value = scanJsonValue(text, cursor);
|
|
194
|
+
duplicate ||= value.duplicate;
|
|
195
|
+
cursor = skipJsonWhitespace(text, value.next);
|
|
196
|
+
if (text[cursor] === "]") return { next: cursor + 1, duplicate };
|
|
197
|
+
if (text[cursor] !== ",") throw new SyntaxError("expected JSON array separator");
|
|
198
|
+
cursor = skipJsonWhitespace(text, cursor + 1);
|
|
199
|
+
}
|
|
200
|
+
throw new SyntaxError("unterminated JSON array");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isUnsafeJsonDocument(text: string): boolean {
|
|
204
|
+
try {
|
|
205
|
+
const result = scanJsonValue(text, 0);
|
|
206
|
+
return result.duplicate || skipJsonWhitespace(text, result.next) !== text.length;
|
|
207
|
+
} catch {
|
|
208
|
+
// Scanner disagreement is untrusted input, just like JSON.parse failure.
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function exactResetEligibleCode(
|
|
214
|
+
container: Record<string, unknown>,
|
|
215
|
+
): CodexResetEligibleExhaustionCode | undefined {
|
|
216
|
+
const hasCode = hasOwnField(container, "code");
|
|
217
|
+
const hasType = hasOwnField(container, "type");
|
|
218
|
+
if (!hasCode && !hasType) return undefined;
|
|
219
|
+
|
|
220
|
+
const code = hasCode ? container.code : undefined;
|
|
221
|
+
const type = hasType ? container.type : undefined;
|
|
222
|
+
if ((hasCode && typeof code !== "string") || (hasType && typeof type !== "string")) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
if (hasCode && hasType && code !== type) return undefined;
|
|
226
|
+
|
|
227
|
+
const value = hasCode ? code : type;
|
|
228
|
+
if (typeof value !== "string") return undefined;
|
|
229
|
+
return RESET_ELIGIBLE_CODES.has(value as CodexResetEligibleExhaustionCode)
|
|
230
|
+
? value as CodexResetEligibleExhaustionCode
|
|
231
|
+
: undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function structuredResetEligibleCode(payload: unknown): CodexResetEligibleExhaustionCode | undefined {
|
|
235
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
236
|
+
const root = payload as Record<string, unknown>;
|
|
237
|
+
const hasRootDiscriminator = hasOwnField(root, "code") || hasOwnField(root, "type");
|
|
238
|
+
|
|
239
|
+
if (!hasOwnField(root, "error")) return exactResetEligibleCode(root);
|
|
240
|
+
if (hasRootDiscriminator) return undefined;
|
|
241
|
+
|
|
242
|
+
const nested = root.error;
|
|
243
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) return undefined;
|
|
244
|
+
return exactResetEligibleCode(nested as Record<string, unknown>);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function resetEligibleCodeFromResponse(
|
|
248
|
+
response: Response,
|
|
249
|
+
signal?: AbortSignal,
|
|
250
|
+
): Promise<CodexResetEligibleExhaustionCode | undefined> {
|
|
251
|
+
try {
|
|
252
|
+
const body = await readBoundedResponseBody(response.clone(), { signal, fatalUtf8: true });
|
|
253
|
+
if (!body.displaySafe || body.truncated || !body.text.trim()) return undefined;
|
|
254
|
+
const payload = JSON.parse(body.text) as unknown;
|
|
255
|
+
// JSON.parse silently keeps the last duplicate key, making contradictory
|
|
256
|
+
// payloads order-dependent. Reject any duplicate at any object depth.
|
|
257
|
+
if (isUnsafeJsonDocument(body.text)) return undefined;
|
|
258
|
+
return structuredResetEligibleCode(payload);
|
|
259
|
+
} catch {
|
|
260
|
+
// Classification must fail closed. A malformed, oversized, consumed, or
|
|
261
|
+
// cancelled body cannot authorize an irreversible reset-credit operation.
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Classify an upstream Codex rejection before any response event is exposed.
|
|
268
|
+
*
|
|
269
|
+
* Only an exact structured exhaustion code on HTTP 429/402 is reset-eligible.
|
|
270
|
+
* Status alone and message text are intentionally insufficient. The broad
|
|
271
|
+
* alternate-account retry remains eligible for 429/402 to preserve #584.
|
|
272
|
+
*/
|
|
273
|
+
export async function classifyCodexPreStreamRejection(
|
|
274
|
+
response: Response,
|
|
275
|
+
options: { signal?: AbortSignal } = {},
|
|
276
|
+
): Promise<CodexPreStreamRejection> {
|
|
277
|
+
const status = response.status;
|
|
278
|
+
if (status === 401) return rejection(status, "authentication-error");
|
|
279
|
+
if (status === 403) {
|
|
280
|
+
const denial = await denialFromResponse(response, options.signal);
|
|
281
|
+
return { ...rejection(status, "permission-error"), ...(denial ? { denial } : {}) };
|
|
282
|
+
}
|
|
283
|
+
if (TRANSIENT_SERVER_STATUSES.has(status)) return rejection(status, "transient-server-error");
|
|
284
|
+
if (status !== 429 && status !== 402) return rejection(status, "other");
|
|
285
|
+
|
|
286
|
+
const semanticCode = await resetEligibleCodeFromResponse(response, options.signal);
|
|
287
|
+
if (semanticCode) {
|
|
288
|
+
return rejection(status, "reset-eligible-exhaustion", {
|
|
289
|
+
alternateRetryEligible: true,
|
|
290
|
+
semanticCode,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return rejection(
|
|
294
|
+
status,
|
|
295
|
+
status === 429 ? "generic-rate-limit" : "unverified-billing-or-quota",
|
|
296
|
+
{ alternateRetryEligible: true },
|
|
297
|
+
);
|
|
298
|
+
}
|