@iislee/opencodex 2.11.0 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -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 +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -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 +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- 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 +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- 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 +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- 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 +618 -74
- package/src/adapters/identity.ts +39 -6
- 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 +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- 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 +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- 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 +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- 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 +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -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 +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- 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 +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -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 +80 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +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 +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +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 +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -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 +152 -29
- package/src/images/plan.ts +23 -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.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- 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 +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- 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/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- 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/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 +202 -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 +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -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/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- 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/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -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 +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -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 +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -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 +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -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 +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- 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 +865 -181
- package/src/server/lifecycle.ts +284 -13
- 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 +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -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 +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +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 +288 -32
- 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 +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -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 +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -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 +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- 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 +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- 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 +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- 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 +9 -3
- 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 +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- 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
|
@@ -13,7 +13,33 @@ import {
|
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { adminApiTokenFilePath } from "../lib/admin-secrets";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
LOCAL_MANAGEMENT_CAPABILITY_HEADER,
|
|
18
|
+
LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
|
|
19
|
+
LOCAL_MANAGEMENT_EXPECTED_PID_HEADER,
|
|
20
|
+
LOCAL_MANAGEMENT_NONCE_HEADER,
|
|
21
|
+
parseExpectedLocalManagementPid,
|
|
22
|
+
verifyLocalManagementReadCapability,
|
|
23
|
+
} from "../lib/local-management-capability";
|
|
24
|
+
import {
|
|
25
|
+
SYSTEM_RESTART_CAPABILITY_HEADER,
|
|
26
|
+
SYSTEM_RESTART_EXPECTED_PID_HEADER,
|
|
27
|
+
SYSTEM_RESTART_NONCE_HEADER,
|
|
28
|
+
SYSTEM_RESTART_PATH,
|
|
29
|
+
parseExpectedSystemRestartPid,
|
|
30
|
+
verifySystemRestartCapability,
|
|
31
|
+
} from "../lib/system-restart-contract";
|
|
32
|
+
import {
|
|
33
|
+
LOCAL_PROVIDER_RELOAD_CAPABILITY_HEADER,
|
|
34
|
+
LOCAL_PROVIDER_RELOAD_EXPECTED_PID_HEADER,
|
|
35
|
+
LOCAL_PROVIDER_RELOAD_EXPIRES_AT_HEADER,
|
|
36
|
+
LOCAL_PROVIDER_RELOAD_NAME_HEADER,
|
|
37
|
+
LOCAL_PROVIDER_RELOAD_NONCE_HEADER,
|
|
38
|
+
LOCAL_PROVIDER_RELOAD_PATH,
|
|
39
|
+
parseExpectedLocalProviderReloadPid,
|
|
40
|
+
verifyLocalProviderReloadCapability,
|
|
41
|
+
} from "../lib/local-provider-reload-contract";
|
|
42
|
+
import { forgetEphemeralSecretPath, forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
17
43
|
import type { OcxConfig } from "../types";
|
|
18
44
|
import {
|
|
19
45
|
isAllowedManagementOrigin,
|
|
@@ -26,6 +52,12 @@ import {
|
|
|
26
52
|
|
|
27
53
|
const GUI_SESSION_TTL_MS = 5 * 60_000;
|
|
28
54
|
const GUI_SESSION_LIMIT = 128;
|
|
55
|
+
const LOCAL_READ_REPLAY_LIMIT = 256;
|
|
56
|
+
const consumedLocalReadCapabilities = new Map<string, number>();
|
|
57
|
+
const admittedLocalReadRequests = new WeakSet<Request>();
|
|
58
|
+
const LOCAL_PROVIDER_RELOAD_REPLAY_LIMIT = 256;
|
|
59
|
+
const consumedLocalProviderReloadCapabilities = new Map<string, number>();
|
|
60
|
+
const admittedLocalProviderReloadRequests = new WeakSet<Request>();
|
|
29
61
|
|
|
30
62
|
interface GuiSessionRecord {
|
|
31
63
|
csrfToken: string;
|
|
@@ -95,12 +127,17 @@ function readExistingToken(path: string): string {
|
|
|
95
127
|
export function removeManagementTokenPathBestEffort(
|
|
96
128
|
path: string,
|
|
97
129
|
remove: (path: string) => void = unlinkSync,
|
|
130
|
+
options?: { ephemeral?: boolean },
|
|
98
131
|
): void {
|
|
132
|
+
// Temps get the full ephemeral release (success + both timeout namespaces);
|
|
133
|
+
// stable token paths drop only the success memo — destination-keyed timeout
|
|
134
|
+
// memos are intentional anti-restall state.
|
|
135
|
+
const forget = options?.ephemeral ? forgetEphemeralSecretPath : forgetHardenedSecretPath;
|
|
99
136
|
try {
|
|
100
137
|
remove(path);
|
|
101
|
-
|
|
138
|
+
forget(path);
|
|
102
139
|
} catch (error) {
|
|
103
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
140
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") forget(path);
|
|
104
141
|
/* other failures retain fail-closed state for the caller */
|
|
105
142
|
}
|
|
106
143
|
}
|
|
@@ -120,7 +157,8 @@ function createTokenFile(path: string): string {
|
|
|
120
157
|
chmodSync(temporary, 0o600);
|
|
121
158
|
let temporaryHardened: { ok: boolean };
|
|
122
159
|
try {
|
|
123
|
-
|
|
160
|
+
// Destination-keyed timeout memo (the final token path), not the temp.
|
|
161
|
+
temporaryHardened = hardenSecretPath(temporary, { required: true, timeoutMemoKey: path });
|
|
124
162
|
} catch {
|
|
125
163
|
temporaryHardened = { ok: false };
|
|
126
164
|
}
|
|
@@ -155,7 +193,7 @@ function createTokenFile(path: string): string {
|
|
|
155
193
|
if (fd !== null) {
|
|
156
194
|
try { closeSync(fd); } catch { /* best effort */ }
|
|
157
195
|
}
|
|
158
|
-
removeManagementTokenPathBestEffort(temporary);
|
|
196
|
+
removeManagementTokenPathBestEffort(temporary, unlinkSync, { ephemeral: true });
|
|
159
197
|
}
|
|
160
198
|
}
|
|
161
199
|
|
|
@@ -231,11 +269,188 @@ export function issueGuiSession(
|
|
|
231
269
|
return { token, ...session };
|
|
232
270
|
}
|
|
233
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Which credential actually authorized a management request.
|
|
274
|
+
*
|
|
275
|
+
* `admin-token` is the raw token from disk/env: anything running as the user can
|
|
276
|
+
* read it, including a coding agent. `gui-session` is a session token this process
|
|
277
|
+
* minted for a browser, and it only authorizes a mutation after the origin and the
|
|
278
|
+
* per-session CSRF token match. Consent-bearing routes must key off this value
|
|
279
|
+
* rather than off request headers, which the token holder can forge freely.
|
|
280
|
+
* The capability principals are process-scoped HMACs bound to the current process
|
|
281
|
+
* PID and listening port. Local reads are accepted only for two exact GET paths;
|
|
282
|
+
* restart and provider reload remain separate wire contracts for their exact POSTs.
|
|
283
|
+
*/
|
|
284
|
+
export type ManagementPrincipal =
|
|
285
|
+
| "admin-token"
|
|
286
|
+
| "gui-session"
|
|
287
|
+
| "local-read-capability"
|
|
288
|
+
| "local-provider-reload-capability"
|
|
289
|
+
| "system-restart-capability";
|
|
290
|
+
|
|
291
|
+
export interface LocalManagementAuthContext {
|
|
292
|
+
attestationSecret: string;
|
|
293
|
+
pid: number;
|
|
294
|
+
port: number;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function hasSystemRestartCapability(
|
|
298
|
+
req: Request,
|
|
299
|
+
local: LocalManagementAuthContext | undefined,
|
|
300
|
+
): boolean {
|
|
301
|
+
if (!local || req.method !== "POST") return false;
|
|
302
|
+
let path: string;
|
|
303
|
+
try {
|
|
304
|
+
path = new URL(req.url).pathname;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (path !== SYSTEM_RESTART_PATH) return false;
|
|
309
|
+
const expectedPid = parseExpectedSystemRestartPid(
|
|
310
|
+
req.headers.get(SYSTEM_RESTART_EXPECTED_PID_HEADER),
|
|
311
|
+
);
|
|
312
|
+
if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
|
|
313
|
+
return verifySystemRestartCapability(
|
|
314
|
+
local.attestationSecret,
|
|
315
|
+
req.headers.get(SYSTEM_RESTART_NONCE_HEADER),
|
|
316
|
+
req.method,
|
|
317
|
+
path,
|
|
318
|
+
local.pid,
|
|
319
|
+
local.port,
|
|
320
|
+
req.headers.get(SYSTEM_RESTART_CAPABILITY_HEADER),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function hasLocalReadCapability(
|
|
325
|
+
req: Request,
|
|
326
|
+
local: LocalManagementAuthContext | undefined,
|
|
327
|
+
): boolean {
|
|
328
|
+
// requireManagementAuth and managementPrincipal inspect the same Request in
|
|
329
|
+
// sequence. Preserve that one admission without accepting a replayed request.
|
|
330
|
+
if (admittedLocalReadRequests.has(req)) return true;
|
|
331
|
+
if (!local || req.method !== "GET") return false;
|
|
332
|
+
let url: URL;
|
|
333
|
+
try {
|
|
334
|
+
url = new URL(req.url);
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
// Do not let a future query-bearing variant silently inherit this narrow grant.
|
|
339
|
+
if (url.search !== "") return false;
|
|
340
|
+
const expectedPid = parseExpectedLocalManagementPid(
|
|
341
|
+
req.headers.get(LOCAL_MANAGEMENT_EXPECTED_PID_HEADER),
|
|
342
|
+
);
|
|
343
|
+
if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
|
|
344
|
+
const expiresAtRaw = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER);
|
|
345
|
+
if (!expiresAtRaw || !/^[1-9]\d*$/.test(expiresAtRaw)) return false;
|
|
346
|
+
const expiresAt = Number(expiresAtRaw);
|
|
347
|
+
if (!Number.isSafeInteger(expiresAt)) return false;
|
|
348
|
+
const capability = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_HEADER);
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
if (!verifyLocalManagementReadCapability(
|
|
351
|
+
local.attestationSecret,
|
|
352
|
+
req.headers.get(LOCAL_MANAGEMENT_NONCE_HEADER),
|
|
353
|
+
req.method,
|
|
354
|
+
url.pathname,
|
|
355
|
+
local.pid,
|
|
356
|
+
local.port,
|
|
357
|
+
expiresAt,
|
|
358
|
+
capability,
|
|
359
|
+
now,
|
|
360
|
+
)) return false;
|
|
361
|
+
for (const [consumed, retainedUntil] of consumedLocalReadCapabilities) {
|
|
362
|
+
if (retainedUntil <= now) consumedLocalReadCapabilities.delete(consumed);
|
|
363
|
+
}
|
|
364
|
+
if (!capability || consumedLocalReadCapabilities.has(capability)) return false;
|
|
365
|
+
if (consumedLocalReadCapabilities.size >= LOCAL_READ_REPLAY_LIMIT) return false;
|
|
366
|
+
consumedLocalReadCapabilities.set(capability, expiresAt);
|
|
367
|
+
admittedLocalReadRequests.add(req);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function hasLocalProviderReloadCapability(
|
|
372
|
+
req: Request,
|
|
373
|
+
local: LocalManagementAuthContext | undefined,
|
|
374
|
+
): boolean {
|
|
375
|
+
if (admittedLocalProviderReloadRequests.has(req)) return true;
|
|
376
|
+
if (!local || req.method !== "POST") return false;
|
|
377
|
+
let url: URL;
|
|
378
|
+
try {
|
|
379
|
+
url = new URL(req.url);
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (url.pathname !== LOCAL_PROVIDER_RELOAD_PATH || url.search !== "") return false;
|
|
384
|
+
const contentLength = req.headers.get("content-length");
|
|
385
|
+
if (contentLength !== "0" || req.headers.has("transfer-encoding")) return false;
|
|
386
|
+
const expectedPid = parseExpectedLocalProviderReloadPid(
|
|
387
|
+
req.headers.get(LOCAL_PROVIDER_RELOAD_EXPECTED_PID_HEADER),
|
|
388
|
+
);
|
|
389
|
+
if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
|
|
390
|
+
const expiresAtRaw = req.headers.get(LOCAL_PROVIDER_RELOAD_EXPIRES_AT_HEADER);
|
|
391
|
+
if (!expiresAtRaw || !/^[1-9]\d*$/.test(expiresAtRaw)) return false;
|
|
392
|
+
const expiresAt = Number(expiresAtRaw);
|
|
393
|
+
if (!Number.isSafeInteger(expiresAt)) return false;
|
|
394
|
+
const name = req.headers.get(LOCAL_PROVIDER_RELOAD_NAME_HEADER);
|
|
395
|
+
const capability = req.headers.get(LOCAL_PROVIDER_RELOAD_CAPABILITY_HEADER);
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
if (!verifyLocalProviderReloadCapability(
|
|
398
|
+
local.attestationSecret,
|
|
399
|
+
req.headers.get(LOCAL_PROVIDER_RELOAD_NONCE_HEADER),
|
|
400
|
+
req.method,
|
|
401
|
+
url.pathname,
|
|
402
|
+
name,
|
|
403
|
+
local.pid,
|
|
404
|
+
local.port,
|
|
405
|
+
expiresAt,
|
|
406
|
+
capability,
|
|
407
|
+
now,
|
|
408
|
+
)) return false;
|
|
409
|
+
for (const [consumed, retainedUntil] of consumedLocalProviderReloadCapabilities) {
|
|
410
|
+
if (retainedUntil <= now) consumedLocalProviderReloadCapabilities.delete(consumed);
|
|
411
|
+
}
|
|
412
|
+
if (!capability || consumedLocalProviderReloadCapabilities.has(capability)) return false;
|
|
413
|
+
if (consumedLocalProviderReloadCapabilities.size >= LOCAL_PROVIDER_RELOAD_REPLAY_LIMIT) return false;
|
|
414
|
+
consumedLocalProviderReloadCapabilities.set(capability, expiresAt);
|
|
415
|
+
admittedLocalProviderReloadRequests.add(req);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The principal for a request that already passed `requireManagementAuth`. Kept as a
|
|
421
|
+
* separate resolution (rather than a changed return type) so every existing caller
|
|
422
|
+
* keeps its `Response | null` contract. Browser and admin principals are derived
|
|
423
|
+
* from the same session table and CSRF comparison the gate uses; the restart
|
|
424
|
+
* principal is derived from the same process-scoped capability check.
|
|
425
|
+
*/
|
|
426
|
+
export function managementPrincipal(
|
|
427
|
+
req: Request,
|
|
428
|
+
state: ManagementAuthState,
|
|
429
|
+
config?: OcxConfig,
|
|
430
|
+
local?: LocalManagementAuthContext,
|
|
431
|
+
): ManagementPrincipal | null {
|
|
432
|
+
if (hasSystemRestartCapability(req, local)) return "system-restart-capability";
|
|
433
|
+
if (hasLocalProviderReloadCapability(req, local)) return "local-provider-reload-capability";
|
|
434
|
+
if (hasLocalReadCapability(req, local)) return "local-read-capability";
|
|
435
|
+
if (!state.available) return null;
|
|
436
|
+
const actual = req.headers.get("x-opencodex-api-key")?.trim()
|
|
437
|
+
|| req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
438
|
+
if (!actual) return null;
|
|
439
|
+
if (equalSecret(actual, state.token)) return "admin-token";
|
|
440
|
+
if (!config) return null;
|
|
441
|
+
removeExpiredSessions(state);
|
|
442
|
+
return state.sessions.has(actual) ? "gui-session" : null;
|
|
443
|
+
}
|
|
444
|
+
|
|
234
445
|
export function requireManagementAuth(
|
|
235
446
|
req: Request,
|
|
236
447
|
state: ManagementAuthState,
|
|
237
448
|
config?: OcxConfig,
|
|
449
|
+
local?: LocalManagementAuthContext,
|
|
238
450
|
): Response | null {
|
|
451
|
+
if (hasSystemRestartCapability(req, local)) return null;
|
|
452
|
+
if (hasLocalProviderReloadCapability(req, local)) return null;
|
|
453
|
+
if (hasLocalReadCapability(req, local)) return null;
|
|
239
454
|
if (!state.available) {
|
|
240
455
|
return Response.json({
|
|
241
456
|
error: "management API unavailable",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional per-attempt route-identity linker.
|
|
3
|
+
*
|
|
4
|
+
* Compatibility Lab attaches an opaque route-subject digest to request attempts so its
|
|
5
|
+
* passive-production surface (CL-09) can correlate them later. That is an opt-in
|
|
6
|
+
* subsystem, so the core request path holds only a slot: null on installs that never
|
|
7
|
+
* activate Lab, which is every install without a routing profile.
|
|
8
|
+
*
|
|
9
|
+
* Contract for any registered implementation: synchronous, free of side effects with
|
|
10
|
+
* respect to the request, and non-throwing. The upstream request must never be delayed,
|
|
11
|
+
* retried, or altered by identity linkage. The try/catch lives here rather than at the
|
|
12
|
+
* call site so the guarantee belongs to the mechanism instead of being restated by every
|
|
13
|
+
* caller.
|
|
14
|
+
*
|
|
15
|
+
* See devlog/_fin/260814_lab_core_decoupling/020_request_path_gate.md
|
|
16
|
+
*/
|
|
17
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
18
|
+
import type { InboundWire } from "../providers/registry";
|
|
19
|
+
|
|
20
|
+
export type PassiveRouteLinker = (
|
|
21
|
+
config: OcxConfig,
|
|
22
|
+
providerName: string,
|
|
23
|
+
modelId: string,
|
|
24
|
+
routed: OcxProviderConfig,
|
|
25
|
+
inboundWire: InboundWire,
|
|
26
|
+
) => string | null;
|
|
27
|
+
|
|
28
|
+
let linker: PassiveRouteLinker | null = null;
|
|
29
|
+
|
|
30
|
+
/** Install the linker. Returns a detach function. */
|
|
31
|
+
export function setPassiveRouteLinker(next: PassiveRouteLinker): () => void {
|
|
32
|
+
linker = next;
|
|
33
|
+
return () => {
|
|
34
|
+
// Only detach our own registration: a later activation may have replaced it.
|
|
35
|
+
if (linker === next) linker = null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the attempt identity, or null when no subsystem is active.
|
|
41
|
+
* Never throws: linkage is best-effort metadata and must not affect the request.
|
|
42
|
+
*/
|
|
43
|
+
export function resolvePassiveRouteSubjectId(
|
|
44
|
+
config: OcxConfig,
|
|
45
|
+
providerName: string,
|
|
46
|
+
modelId: string,
|
|
47
|
+
routed: OcxProviderConfig,
|
|
48
|
+
inboundWire: InboundWire,
|
|
49
|
+
): string | null {
|
|
50
|
+
if (!linker) return null;
|
|
51
|
+
try {
|
|
52
|
+
return linker(config, providerName, modelId, routed, inboundWire);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True when an optional subsystem has installed a linker. Test/diagnostic use. */
|
|
59
|
+
export function hasPassiveRouteLinker(): boolean {
|
|
60
|
+
return linker !== null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Test-only reset. */
|
|
64
|
+
export function resetPassiveRouteLinkerForTests(): void {
|
|
65
|
+
linker = null;
|
|
66
|
+
}
|
package/src/server/ports.ts
CHANGED
|
@@ -57,6 +57,16 @@ export type FindAvailablePortOptions = {
|
|
|
57
57
|
* update restart cannot hop to a random ephemeral listener (PR #152 gap).
|
|
58
58
|
*/
|
|
59
59
|
allowEphemeralFallback?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* A port this selection must never return, even when it is free (#1102).
|
|
62
|
+
*
|
|
63
|
+
* The unauthenticated loopback listener binds a fixed port from config. If the public
|
|
64
|
+
* listener took that port first — via an explicit `--port`, a `config.port` of 0, or the
|
|
65
|
+
* ephemeral fallback happening to land on it — the loopback bind would then fail with
|
|
66
|
+
* EADDRINUSE, and the startup transaction would roll back a public listener that had
|
|
67
|
+
* nothing wrong with it. Excluding the port here fails the right thing at the right time.
|
|
68
|
+
*/
|
|
69
|
+
reservedPort?: number;
|
|
60
70
|
};
|
|
61
71
|
|
|
62
72
|
export class PortUnavailableError extends Error {
|
|
@@ -75,6 +85,12 @@ export async function findAvailablePort(
|
|
|
75
85
|
): Promise<number> {
|
|
76
86
|
const preferRetryMs = opts.preferRetryMs ?? 0;
|
|
77
87
|
const allowEphemeral = opts.allowEphemeralFallback !== false;
|
|
88
|
+
const reserved = opts.reservedPort;
|
|
89
|
+
// An explicit preference for the reserved port is a configuration mistake, not a busy
|
|
90
|
+
// socket: retrying or hopping would hide it. Refuse before probing anything.
|
|
91
|
+
if (reserved !== undefined && preferredPort === reserved) {
|
|
92
|
+
throw new PortUnavailableError(preferredPort, hostname);
|
|
93
|
+
}
|
|
78
94
|
// Port 0 asks the OS to select an ephemeral port. Resolve it to that concrete
|
|
79
95
|
// port here so callers never persist or advertise an unusable `:0` endpoint.
|
|
80
96
|
if (preferredPort > 0 && preferRetryMs > 0) {
|
|
@@ -92,7 +108,31 @@ export async function findAvailablePort(
|
|
|
92
108
|
throw new PortUnavailableError(preferredPort, hostname);
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
// Bounded, not recursive. The OS can hand back the reserved port, and a redraw practically
|
|
112
|
+
// always differs — but "practically always" is not a termination argument, and an unbounded
|
|
113
|
+
// async recursion has no way to stop if the assumption is ever wrong.
|
|
114
|
+
for (let attempt = 0; attempt < EPHEMERAL_REDRAW_LIMIT; attempt += 1) {
|
|
115
|
+
const port = await allocateEphemeralPort(hostname);
|
|
116
|
+
if (port !== reserved) return port;
|
|
117
|
+
}
|
|
118
|
+
throw new Error("failed to allocate an available port");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** How many times an ephemeral draw may come back reserved before we give up. */
|
|
122
|
+
const EPHEMERAL_REDRAW_LIMIT = 8;
|
|
123
|
+
|
|
124
|
+
/** Test seam: replace the OS ephemeral allocator so the redraw path is reachable. */
|
|
125
|
+
let ephemeralAllocator: ((hostname: string) => Promise<number>) | null = null;
|
|
126
|
+
|
|
127
|
+
export function setEphemeralPortAllocatorForTests(
|
|
128
|
+
allocator: ((hostname: string) => Promise<number>) | null,
|
|
129
|
+
): void {
|
|
130
|
+
ephemeralAllocator = allocator;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function allocateEphemeralPort(hostname: string): Promise<number> {
|
|
134
|
+
if (ephemeralAllocator) return ephemeralAllocator(hostname);
|
|
135
|
+
return await new Promise<number>((resolve, reject) => {
|
|
96
136
|
const server = createServer();
|
|
97
137
|
server.once("error", reject);
|
|
98
138
|
server.once("listening", () => {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
|
|
11
11
|
*/
|
|
12
12
|
import { loadConfig, readAlivePid, readRuntimePort, verifyPidIdentity } from "../config";
|
|
13
|
+
import { directLocalHttpFetch } from "./direct-local-http";
|
|
13
14
|
|
|
14
15
|
export interface HealthzIdentity {
|
|
15
16
|
service?: unknown;
|
|
@@ -17,6 +18,9 @@ export interface HealthzIdentity {
|
|
|
17
18
|
version?: unknown;
|
|
18
19
|
uptime?: unknown;
|
|
19
20
|
pid?: unknown;
|
|
21
|
+
port?: unknown;
|
|
22
|
+
restartCapability?: unknown;
|
|
23
|
+
providerReloadCapability?: unknown;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export interface LivenessIo {
|
|
@@ -47,6 +51,9 @@ export interface LivenessIo {
|
|
|
47
51
|
nowFn?: () => number;
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
/** Default per-probe fetch ceiling shared by liveness and readiness probes. */
|
|
55
|
+
export const DEFAULT_PROBE_TIMEOUT_MS = 750;
|
|
56
|
+
|
|
50
57
|
/** Default probe options for service stop / orphan cleanup — a just-bound proxy can miss a single 750ms probe. */
|
|
51
58
|
export const SERVICE_STOP_LIVENESS: Pick<LivenessIo, "timeoutMs" | "attempts"> = {
|
|
52
59
|
timeoutMs: 1500,
|
|
@@ -92,10 +99,10 @@ export async function proxyIdentityAt(
|
|
|
92
99
|
opts: { hostname?: string; expectedPid?: number } = {},
|
|
93
100
|
io: LivenessIo = {},
|
|
94
101
|
): Promise<{ pid: number | null } | null> {
|
|
95
|
-
const fetchFn = io.fetchFn ??
|
|
102
|
+
const fetchFn = io.fetchFn ?? directLocalHttpFetch;
|
|
96
103
|
const sleepFn = io.sleepFn ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
97
104
|
const nowFn = io.nowFn ?? Date.now;
|
|
98
|
-
const baseTimeoutMs = io.timeoutMs ??
|
|
105
|
+
const baseTimeoutMs = io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
99
106
|
const requestedAttempts = Math.trunc(io.attempts ?? 1);
|
|
100
107
|
const attempts = Number.isNaN(requestedAttempts)
|
|
101
108
|
? 1
|
|
@@ -138,6 +145,11 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
138
145
|
const verifyPidFn = io.verifyPidFn ?? verifyPidIdentity;
|
|
139
146
|
const readRuntimeFn = io.readRuntimeFn ?? readRuntimePort;
|
|
140
147
|
const configFn = io.configFn ?? loadConfig;
|
|
148
|
+
const nowFn = io.nowFn ?? Date.now;
|
|
149
|
+
const deadlineAt = io.deadlineAt;
|
|
150
|
+
const probeIo: LivenessIo = io;
|
|
151
|
+
const budgetExhausted = (): boolean =>
|
|
152
|
+
deadlineAt !== undefined && nowFn() >= deadlineAt;
|
|
141
153
|
|
|
142
154
|
// The cheap pid is discovery-only. Before it can appear in a returned (killable) result
|
|
143
155
|
// it must pass the full identity check AND the verifier must echo the exact candidate —
|
|
@@ -160,8 +172,9 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
160
172
|
if (pid) {
|
|
161
173
|
const runtime = readRuntimeFn(pid);
|
|
162
174
|
if (runtime?.port) {
|
|
175
|
+
if (budgetExhausted()) return null;
|
|
163
176
|
probedPort = runtime.port;
|
|
164
|
-
const identity = await proxyIdentityAt(runtime.port, { hostname: runtime.hostname, expectedPid: pid },
|
|
177
|
+
const identity = await proxyIdentityAt(runtime.port, { hostname: runtime.hostname, expectedPid: pid }, probeIo);
|
|
165
178
|
if (identity) {
|
|
166
179
|
// healthz confirmed the pid itself → trusted; a pidless legacy body did not,
|
|
167
180
|
// so the cheap pid must pass full identity verification before it is returned.
|
|
@@ -176,8 +189,9 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
176
189
|
// identity-probe it so ensure/update/stop see the live proxy instead of shadowing it.
|
|
177
190
|
const record = readRuntimeFn();
|
|
178
191
|
if (record?.port && record.port !== probedPort) {
|
|
192
|
+
if (budgetExhausted()) return null;
|
|
179
193
|
const expectedPid = typeof record.pid === "number" ? record.pid : undefined;
|
|
180
|
-
const identity = await proxyIdentityAt(record.port, { hostname: record.hostname, expectedPid },
|
|
194
|
+
const identity = await proxyIdentityAt(record.port, { hostname: record.hostname, expectedPid }, probeIo);
|
|
181
195
|
// Only the healthz-reported pid is authoritative here. The record's pid may be stale
|
|
182
196
|
// (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
|
|
183
197
|
// would hand destructive callers (stopProxy → kill fallback) a reusable pid.
|
|
@@ -188,7 +202,8 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
188
202
|
|
|
189
203
|
const config = configFn();
|
|
190
204
|
const port = config.port ?? 10100;
|
|
191
|
-
|
|
205
|
+
if (budgetExhausted()) return null;
|
|
206
|
+
const identity = await proxyIdentityAt(port, { hostname: config.hostname }, probeIo);
|
|
192
207
|
if (identity) {
|
|
193
208
|
return {
|
|
194
209
|
pid: verifiedReportedPid(identity.pid) ?? killablePid(pid),
|
|
@@ -199,3 +214,115 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
199
214
|
}
|
|
200
215
|
return null;
|
|
201
216
|
}
|
|
217
|
+
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
// Readiness (/readyz) strict probe.
|
|
220
|
+
//
|
|
221
|
+
// Liveness (/healthz) confirms the process answers; readiness confirms the
|
|
222
|
+
// post-startup Codex sync has settled. A readiness probe is identity-checked the
|
|
223
|
+
// same way liveness is, AND additionally enforces the full /readyz contract so
|
|
224
|
+
// an adversarial or malformed body can never count as ready:
|
|
225
|
+
//
|
|
226
|
+
// - HTTP 200 is required for status="ready"; HTTP 503 is required for pending
|
|
227
|
+
// or failed. Any other HTTP/body-status pairing is an invalid contract.
|
|
228
|
+
// - body.service must be exactly "opencodex".
|
|
229
|
+
// - body.version must be a non-empty string.
|
|
230
|
+
// - body.uptime must be a finite nonnegative number.
|
|
231
|
+
// - body.pid must be a positive integer; when `expectedPid` is supplied it must
|
|
232
|
+
// match exactly.
|
|
233
|
+
// - body.port must be an integer in 1..65535 and equal the probed port.
|
|
234
|
+
// - body.status must be exactly one of pending|ready|failed.
|
|
235
|
+
//
|
|
236
|
+
// Any unreachable, foreign, legacy, malformed, mismatched, or self-inconsistent
|
|
237
|
+
// response returns `null` so callers can never treat an invalid identity/contract
|
|
238
|
+
// as ready.
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
interface ReadyzBody {
|
|
242
|
+
service?: unknown;
|
|
243
|
+
version?: unknown;
|
|
244
|
+
uptime?: unknown;
|
|
245
|
+
pid?: unknown;
|
|
246
|
+
port?: unknown;
|
|
247
|
+
status?: unknown;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface ReadinessProbeResult {
|
|
251
|
+
/** True ONLY for a valid 200 + status="ready" body with a matching pid. */
|
|
252
|
+
ready: boolean;
|
|
253
|
+
/** Fixed sanitized status. A foreign/unreadable body yields a `null` RESULT, never a `null` status. */
|
|
254
|
+
status: "ready" | "pending" | "failed";
|
|
255
|
+
/** Positive integer pid from a valid body. */
|
|
256
|
+
pid: number;
|
|
257
|
+
/** Integer port from a valid body. */
|
|
258
|
+
port: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface ReadinessProbeIo {
|
|
262
|
+
fetchFn?: typeof fetch;
|
|
263
|
+
timeoutMs?: number;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const READYZ_STATUS_VALUES = new Set<"ready" | "pending" | "failed">(["ready", "pending", "failed"]);
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Validate a parsed /readyz body against the strict contract. Returns the
|
|
270
|
+
* sanitized probe result, or `null` when the body is foreign, legacy,
|
|
271
|
+
* malformed, or fails the pid/port checks. Pure (no I/O) so it is fully
|
|
272
|
+
* deterministic and unit-testable.
|
|
273
|
+
*/
|
|
274
|
+
export function validateReadyzBody(
|
|
275
|
+
body: unknown,
|
|
276
|
+
port: number,
|
|
277
|
+
opts: { expectedPid?: number } = {},
|
|
278
|
+
): ReadinessProbeResult | null {
|
|
279
|
+
if (!body || typeof body !== "object") return null;
|
|
280
|
+
const b = body as ReadyzBody;
|
|
281
|
+
if (b.service !== "opencodex") return null;
|
|
282
|
+
if (typeof b.version !== "string" || b.version.length === 0) return null;
|
|
283
|
+
if (typeof b.uptime !== "number" || !Number.isFinite(b.uptime) || b.uptime < 0) return null;
|
|
284
|
+
if (typeof b.pid !== "number" || !Number.isInteger(b.pid) || b.pid <= 0) return null;
|
|
285
|
+
if (
|
|
286
|
+
typeof b.port !== "number"
|
|
287
|
+
|| !Number.isInteger(b.port)
|
|
288
|
+
|| b.port < 1
|
|
289
|
+
|| b.port > 65535
|
|
290
|
+
|| b.port !== port
|
|
291
|
+
) return null;
|
|
292
|
+
if (typeof b.status !== "string" || !READYZ_STATUS_VALUES.has(b.status as "ready" | "pending" | "failed")) return null;
|
|
293
|
+
const status = b.status as "ready" | "pending" | "failed";
|
|
294
|
+
if (opts.expectedPid !== undefined && b.pid !== opts.expectedPid) return null;
|
|
295
|
+
return { ready: status === "ready", status, pid: b.pid, port: b.port };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Identity- and contract-checked /readyz probe. Returns `null` when the
|
|
300
|
+
* endpoint is unreachable or the body fails the strict contract (foreign 200,
|
|
301
|
+
* legacy health-only body, non-JSON, missing/malformed/mismatched fields,
|
|
302
|
+
* wrong port/pid, or an HTTP/body-status inconsistency). Returns
|
|
303
|
+
* `{ready:false, ...}` when the body is ours but pending or failed. Returns
|
|
304
|
+
* `{ready:true, ...}` ONLY for a valid 200 body with `status:"ready"` and (when
|
|
305
|
+
* requested) a matching pid.
|
|
306
|
+
*/
|
|
307
|
+
export async function probeReadiness(
|
|
308
|
+
port: number,
|
|
309
|
+
opts: { hostname?: string; expectedPid?: number } = {},
|
|
310
|
+
io: ReadinessProbeIo = {},
|
|
311
|
+
): Promise<ReadinessProbeResult | null> {
|
|
312
|
+
const fetchFn = io.fetchFn ?? directLocalHttpFetch;
|
|
313
|
+
try {
|
|
314
|
+
const res = await fetchFn(`http://${probeHostname(opts.hostname)}:${port}/readyz`, {
|
|
315
|
+
signal: AbortSignal.timeout(io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS),
|
|
316
|
+
});
|
|
317
|
+
// Parse even on 503: /readyz returns JSON with a sanitized status while pending.
|
|
318
|
+
const body = (await res.json().catch(() => null)) as unknown;
|
|
319
|
+
const parsed = validateReadyzBody(body, port, opts);
|
|
320
|
+
if (!parsed) return null;
|
|
321
|
+
// HTTP/body-status consistency: ready requires 200; pending/failed require 503.
|
|
322
|
+
if (parsed.status === "ready" && res.status !== 200) return null;
|
|
323
|
+
if (parsed.status !== "ready" && res.status !== 503) return null;
|
|
324
|
+
return parsed;
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|