@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
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the service manager says about this machine, read-only and fail-closed.
|
|
3
|
+
*
|
|
4
|
+
* The question this answers is NOT "is a job loaded". Installation writes the
|
|
5
|
+
* definition BEFORE the state file (`service.ts` install paths) and embeds
|
|
6
|
+
* `CODEX_HOME`/`OPENCODEX_HOME` inside it, so an interrupted reinstall leaves a
|
|
7
|
+
* valid state file for one home beside an installed definition for another. A
|
|
8
|
+
* probe that only asked about registration would call that owned. On macOS it is
|
|
9
|
+
* worse: a logged-out user has the plist on disk with no GUI domain at all, so
|
|
10
|
+
* registration reports nothing while a foreign definition sits right there.
|
|
11
|
+
*
|
|
12
|
+
* So the probe reads the DEFINITION, parses the homes out of it, and reports
|
|
13
|
+
* what it saw. Comparing those homes is the caller's job — a probe that returned
|
|
14
|
+
* a verdict would be deciding ownership from half the evidence.
|
|
15
|
+
*
|
|
16
|
+
* Every command here is read-only and bounded. The user's proxy runs under a
|
|
17
|
+
* service manager while this executes; a probe that could start, stop or reload
|
|
18
|
+
* anything is not a probe. An unbounded one is not much better, since a wedged
|
|
19
|
+
* manager would hold the event loop.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join, win32 as win32Path } from "node:path";
|
|
25
|
+
import {
|
|
26
|
+
resolveTrustedWindowsSchtasksExe,
|
|
27
|
+
resolveTrustedWindowsSystemDirectory,
|
|
28
|
+
} from "./lib/windows-elevation";
|
|
29
|
+
import { decodeWindowsTextBytes } from "./lib/windows-text";
|
|
30
|
+
import { WINSW_SERVICE_ID } from "./lib/winsw";
|
|
31
|
+
|
|
32
|
+
/** Short: this runs inside admission, and a slow answer is the same as none. */
|
|
33
|
+
export const SERVICE_PROBE_TIMEOUT_MS = 2_000;
|
|
34
|
+
|
|
35
|
+
export type ServiceManagerBackend = "launchd" | "systemd" | "scheduler" | "winsw";
|
|
36
|
+
|
|
37
|
+
export interface ServiceManagerClaim {
|
|
38
|
+
readonly backend: ServiceManagerBackend;
|
|
39
|
+
readonly definitionPath: string;
|
|
40
|
+
/**
|
|
41
|
+
* The homes the definition names. `null` means the definition deliberately
|
|
42
|
+
* omits that key, which is different from naming a different one: an install
|
|
43
|
+
* that ran without `CODEX_HOME` set writes no such key at all.
|
|
44
|
+
*/
|
|
45
|
+
readonly homes: {
|
|
46
|
+
readonly codexHome: string | null;
|
|
47
|
+
readonly opencodexHome: string | null;
|
|
48
|
+
};
|
|
49
|
+
readonly registration: "present" | "absent";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ServiceManagerInstallation =
|
|
53
|
+
| { readonly kind: "absent" }
|
|
54
|
+
| { readonly kind: "present"; readonly claims: readonly ServiceManagerClaim[] }
|
|
55
|
+
| { readonly kind: "conflict"; readonly claims: readonly ServiceManagerClaim[] }
|
|
56
|
+
| { readonly kind: "unknown"; readonly reason: string };
|
|
57
|
+
|
|
58
|
+
/** Injected so a test can observe the EXACT argv production emits. */
|
|
59
|
+
export interface ProbeRunner {
|
|
60
|
+
(file: string, args: readonly string[]): {
|
|
61
|
+
status: number | null;
|
|
62
|
+
stdout: string;
|
|
63
|
+
stderr: string;
|
|
64
|
+
timedOut: boolean;
|
|
65
|
+
spawnFailed: boolean;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Windows probe runner: preserves schtasks stdout/stderr as raw bytes so the
|
|
71
|
+
* UTF-16LE task XML is not corrupted by a UTF-8 decode.
|
|
72
|
+
*/
|
|
73
|
+
export type RawProbeRunner = (file: string, args: readonly string[]) => {
|
|
74
|
+
status: number | null;
|
|
75
|
+
stdout: Buffer;
|
|
76
|
+
stderr: Buffer;
|
|
77
|
+
timedOut: boolean;
|
|
78
|
+
spawnFailed: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const defaultProbeRunner: ProbeRunner = (file, args) => {
|
|
82
|
+
const result = spawnSync(file, [...args], {
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
timeout: SERVICE_PROBE_TIMEOUT_MS,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
status: result.status,
|
|
89
|
+
stdout: String(result.stdout ?? ""),
|
|
90
|
+
stderr: String(result.stderr ?? ""),
|
|
91
|
+
// `signal` is SIGTERM when the timeout fired; a spawn failure sets `error`.
|
|
92
|
+
timedOut: result.signal !== null && result.error === undefined,
|
|
93
|
+
spawnFailed: result.error !== undefined,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const defaultRawProbeRunner: RawProbeRunner = (file, args) => {
|
|
98
|
+
const result = spawnSync(file, [...args], {
|
|
99
|
+
encoding: "buffer",
|
|
100
|
+
windowsHide: true,
|
|
101
|
+
timeout: SERVICE_PROBE_TIMEOUT_MS,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
status: result.status,
|
|
105
|
+
stdout: Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.alloc(0),
|
|
106
|
+
stderr: Buffer.isBuffer(result.stderr) ? result.stderr : Buffer.alloc(0),
|
|
107
|
+
timedOut: result.signal !== null && result.error === undefined,
|
|
108
|
+
spawnFailed: result.error !== undefined,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export interface ProbeDeps {
|
|
113
|
+
readonly run?: ProbeRunner;
|
|
114
|
+
/** Raw-buffer runner for bounded Windows service-manager queries. */
|
|
115
|
+
readonly runRaw?: RawProbeRunner;
|
|
116
|
+
readonly platform?: NodeJS.Platform;
|
|
117
|
+
readonly uid?: number;
|
|
118
|
+
readonly home?: string;
|
|
119
|
+
/** Effective OpenCodex config dir (OPENCODEX_HOME). Overrides `<home>/.opencodex`. */
|
|
120
|
+
readonly configDir?: string;
|
|
121
|
+
/** Test seam for WinSW SCM status. Production uses bounded trusted `sc.exe query`. */
|
|
122
|
+
readonly winswStatus?: () => "started" | "stopped" | "nonexistent" | "unknown";
|
|
123
|
+
/** Test seam for redirected Windows legacy-codepage output. */
|
|
124
|
+
readonly windowsLocale?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const LABEL = "com.opencodex.proxy";
|
|
128
|
+
const TASK = "opencodex-proxy";
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `launchctl print` exits 113 for a service that is not there and 112 when the
|
|
132
|
+
* domain itself cannot be reached — measured against nonexistent targets rather
|
|
133
|
+
* than assumed. Only 113 is an answer; everything else is a failure to ask.
|
|
134
|
+
*/
|
|
135
|
+
const LAUNCHCTL_NO_SUCH_SERVICE = 113;
|
|
136
|
+
/**
|
|
137
|
+
* 112 is an answer about the DOMAIN, not the label.
|
|
138
|
+
*
|
|
139
|
+
* Measured on macOS 27.0: querying a domain with no service name at all still
|
|
140
|
+
* returns it, and a domain that does not exist cannot be running one of our
|
|
141
|
+
* jobs. Treating it as "could not ask" would refuse every write on a fresh
|
|
142
|
+
* headless Mac — no GUI domain, and no installation either.
|
|
143
|
+
*/
|
|
144
|
+
const LAUNCHCTL_NO_SUCH_DOMAIN = 112;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Residue on disk, distinguished from a path that could not be read.
|
|
148
|
+
*
|
|
149
|
+
* `existsSync` answers "no" for a dangling symlink and for a path whose parent
|
|
150
|
+
* denies traversal. Both are residue, not absence — only ENOENT is absence.
|
|
151
|
+
*/
|
|
152
|
+
function artifactPresence(path: string): "present" | "absent" | "unreadable" {
|
|
153
|
+
try {
|
|
154
|
+
lstatSync(path);
|
|
155
|
+
return "present";
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return (error as NodeJS.ErrnoException)?.code === "ENOENT" ? "absent" : "unreadable";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function unknown(reason: string): ServiceManagerInstallation {
|
|
162
|
+
return { kind: "unknown", reason };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Pull `<key>NAME</key><string>VALUE</string>` out of a plist body. */
|
|
166
|
+
function plistEnvValue(body: string, key: string): string | null {
|
|
167
|
+
const match = body.match(
|
|
168
|
+
new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([^<]*)</string>`),
|
|
169
|
+
);
|
|
170
|
+
return match ? match[1] : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Pull `Environment="NAME=VALUE"` (quoted or bare) out of a systemd unit. */
|
|
174
|
+
function unitEnvValue(body: string, key: string): string | null {
|
|
175
|
+
for (const line of body.split("\n")) {
|
|
176
|
+
const match = line.match(new RegExp(`^\\s*Environment=\\s*"?${key}=([^"\\n]*)"?\\s*$`));
|
|
177
|
+
if (match) return match[1];
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Did `systemctl --user` fail because the session bus could not be reached at all?
|
|
184
|
+
*
|
|
185
|
+
* These are the shapes reported on #2114 and #1939. The distinction that matters is
|
|
186
|
+
* "the question never left the machine" versus "systemd answered and said no" — only
|
|
187
|
+
* the former licenses reading the disk instead.
|
|
188
|
+
*
|
|
189
|
+
* **Locale caveat, stated rather than hidden:** systemd localizes these strings, so a
|
|
190
|
+
* non-English host will not match and keeps the old `unknown`. That is the safe
|
|
191
|
+
* direction — it fences rather than admits — but it does mean the fix does not reach
|
|
192
|
+
* every affected user. Forcing `LC_ALL=C` on the probe would remove the caveat and is
|
|
193
|
+
* the obvious follow-up; it is not done here because it changes every systemctl call
|
|
194
|
+
* this module makes, not just this branch.
|
|
195
|
+
*/
|
|
196
|
+
function busUnreachable(stderr: string): boolean {
|
|
197
|
+
const err = stderr.trim();
|
|
198
|
+
return err.includes("Failed to connect to bus")
|
|
199
|
+
|| err.includes("Failed to connect to user scope bus")
|
|
200
|
+
|| err.includes("Failed to get D-Bus connection")
|
|
201
|
+
|| err.includes("DBUS_SESSION_BUS_ADDRESS")
|
|
202
|
+
|| err.includes("System has not been booted with systemd");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Ownership from the unit file alone, for when the bus cannot answer (#2114).
|
|
207
|
+
*
|
|
208
|
+
* A unit file is proof of installation that does not require a running bus, and the homes
|
|
209
|
+
* it names are what ownership is actually decided on. What the disk cannot tell us is
|
|
210
|
+
* whether systemd has the unit LOADED, so this reports `registration: "absent"` — the
|
|
211
|
+
* honest reading of "no running manager has it" — rather than inventing a live state.
|
|
212
|
+
*
|
|
213
|
+
* A foreign home therefore still blocks, which is the whole reason this consults the disk
|
|
214
|
+
* instead of widening the exit code.
|
|
215
|
+
*/
|
|
216
|
+
function systemdUserUnitSearchPaths(home: string): string[] {
|
|
217
|
+
// systemd's user search path is not one directory. Checking only the canonical one and
|
|
218
|
+
// calling the rest absent is a fail-open: with the bus down a foreign unit in any other
|
|
219
|
+
// search dir is invisible, and "no answer" would be read as "no owner".
|
|
220
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME?.trim();
|
|
221
|
+
const xdgData = process.env.XDG_DATA_HOME?.trim();
|
|
222
|
+
const dirs = [
|
|
223
|
+
xdgConfig ? join(xdgConfig, "systemd", "user") : join(home, ".config", "systemd", "user"),
|
|
224
|
+
join(home, ".config", "systemd", "user"),
|
|
225
|
+
xdgData ? join(xdgData, "systemd", "user") : join(home, ".local", "share", "systemd", "user"),
|
|
226
|
+
join(home, ".local", "share", "systemd", "user"),
|
|
227
|
+
];
|
|
228
|
+
return [...new Set(dirs)].map(dir => join(dir, `${TASK}.service`));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function inspectSystemdOffline(home: string): ServiceManagerInstallation {
|
|
232
|
+
const candidates = systemdUserUnitSearchPaths(home);
|
|
233
|
+
const found = candidates.filter(path => artifactPresence(path) === "present");
|
|
234
|
+
if (candidates.some(path => artifactPresence(path) === "unreadable")) {
|
|
235
|
+
return unknown("the session bus is unreachable and a systemd unit could not be read");
|
|
236
|
+
}
|
|
237
|
+
if (found.length === 0) return { kind: "absent" };
|
|
238
|
+
if (found.length > 1) {
|
|
239
|
+
return unknown("the session bus is unreachable and more than one systemd unit file claims this proxy");
|
|
240
|
+
}
|
|
241
|
+
const definitionPath = found[0]!;
|
|
242
|
+
let body: string;
|
|
243
|
+
try {
|
|
244
|
+
body = readFileSync(definitionPath, "utf-8");
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return unknown(`the session bus is unreachable and the systemd unit could not be read: ${String(error)}`);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
kind: "present",
|
|
250
|
+
claims: [{
|
|
251
|
+
backend: "systemd",
|
|
252
|
+
definitionPath,
|
|
253
|
+
homes: {
|
|
254
|
+
codexHome: unitEnvValue(body, "CODEX_HOME"),
|
|
255
|
+
opencodexHome: unitEnvValue(body, "OPENCODEX_HOME"),
|
|
256
|
+
},
|
|
257
|
+
registration: "absent",
|
|
258
|
+
}],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function inspectLaunchd(deps: Required<Pick<ProbeDeps, "run" | "uid" | "home">>): ServiceManagerInstallation {
|
|
263
|
+
const definitionPath = join(deps.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* BOTH domains, because they are independent and hold separate service sets.
|
|
267
|
+
* Measured on macOS 27.0: the shipped agent answers 0 under `gui/<uid>` and
|
|
268
|
+
* 113 under `user/<uid>`. Asking only one leaves the other free to hold a job
|
|
269
|
+
* this probe would then call absent.
|
|
270
|
+
*/
|
|
271
|
+
let registration: "present" | "absent" = "absent";
|
|
272
|
+
let unreachableDomains = 0;
|
|
273
|
+
for (const domain of [`gui/${deps.uid}`, `user/${deps.uid}`]) {
|
|
274
|
+
const printed = deps.run("/bin/launchctl", ["print", `${domain}/${LABEL}`]);
|
|
275
|
+
if (printed.spawnFailed || printed.timedOut) {
|
|
276
|
+
return unknown(`launchctl could not be asked: ${printed.timedOut ? "timed out" : printed.stderr.trim()}`);
|
|
277
|
+
}
|
|
278
|
+
if (printed.status === 0) { registration = "present"; break; }
|
|
279
|
+
if (printed.status === LAUNCHCTL_NO_SUCH_SERVICE) continue;
|
|
280
|
+
if (printed.status === LAUNCHCTL_NO_SUCH_DOMAIN) { unreachableDomains += 1; continue; }
|
|
281
|
+
return unknown(`launchctl print exited ${String(printed.status)}: ${printed.stderr.trim()}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const definition = artifactPresence(definitionPath);
|
|
285
|
+
if (definition === "absent") {
|
|
286
|
+
// No file. A registration without one means launchd holds a definition whose
|
|
287
|
+
// file is gone — real, and not something to resolve unattended.
|
|
288
|
+
if (registration === "present") {
|
|
289
|
+
return unknown("launchd has a job loaded but its plist is missing");
|
|
290
|
+
}
|
|
291
|
+
/*
|
|
292
|
+
* Nothing staged, and every domain either answered "no such service" or does
|
|
293
|
+
* not exist. 112 is an answer ABOUT THE DOMAIN and is label-independent —
|
|
294
|
+
* querying a domain with no service name at all returns it — so an
|
|
295
|
+
* unreachable domain cannot be hiding a job of ours. Calling this `unknown`
|
|
296
|
+
* instead would refuse every write on a fresh headless Mac, which has no
|
|
297
|
+
* GUI domain and no installation either.
|
|
298
|
+
*/
|
|
299
|
+
void unreachableDomains;
|
|
300
|
+
return { kind: "absent" };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let body: string;
|
|
304
|
+
try {
|
|
305
|
+
body = readFileSync(definitionPath, "utf-8");
|
|
306
|
+
} catch (error) {
|
|
307
|
+
// Present-but-unreadable cannot supply homes, and `present` without homes
|
|
308
|
+
// would compare equal to nothing and read as agreement.
|
|
309
|
+
return unknown(`the launchd plist exists but could not be read: ${String(error)}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
kind: "present",
|
|
314
|
+
claims: [{
|
|
315
|
+
backend: "launchd",
|
|
316
|
+
definitionPath,
|
|
317
|
+
homes: {
|
|
318
|
+
codexHome: plistEnvValue(body, "CODEX_HOME"),
|
|
319
|
+
opencodexHome: plistEnvValue(body, "OPENCODEX_HOME"),
|
|
320
|
+
},
|
|
321
|
+
registration,
|
|
322
|
+
}],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function systemdProperty(out: string, key: string): string | null {
|
|
327
|
+
for (const line of out.split("\n")) {
|
|
328
|
+
const match = line.match(new RegExp(`^${key}=(.*)$`));
|
|
329
|
+
if (match) return match[1].trim();
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function inspectSystemd(deps: Required<Pick<ProbeDeps, "run" | "home">>): ServiceManagerInstallation {
|
|
335
|
+
const definitionPath = join(deps.home, ".config", "systemd", "user", `${TASK}.service`);
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* All four properties in one call. LoadState alone is not enough — it is
|
|
339
|
+
* orthogonal to ActiveState — and neither says whether the LOADED bytes match
|
|
340
|
+
* the file. NeedDaemonReload is that signal, and this repository already
|
|
341
|
+
* documents it as the systemd analogue of launchd's stale plist.
|
|
342
|
+
*/
|
|
343
|
+
const shown = deps.run("systemctl", [
|
|
344
|
+
"--user", "show", TASK,
|
|
345
|
+
"-p", "LoadState", "-p", "ActiveState", "-p", "FragmentPath", "-p", "NeedDaemonReload",
|
|
346
|
+
]);
|
|
347
|
+
if (shown.spawnFailed) return { kind: "absent" };
|
|
348
|
+
if (shown.timedOut) return unknown("systemctl could not be asked: timed out");
|
|
349
|
+
if (shown.status !== 0) {
|
|
350
|
+
// A missing unit still exits ZERO and says not-found; a non-zero status means
|
|
351
|
+
// the question never reached the bus.
|
|
352
|
+
//
|
|
353
|
+
// That is evidence about the BUS, not evidence that a foreign service owns this home
|
|
354
|
+
// (#2114). Calling it `unknown` fences native-main for the whole process, so a laptop
|
|
355
|
+
// with no session bus answers every native request with a 503 until `ocx restart`.
|
|
356
|
+
//
|
|
357
|
+
// Widening on the exit code alone would fail open, because with the bus down systemctl
|
|
358
|
+
// cannot see a foreign unit either. So ask the disk, which needs no bus, and fall back
|
|
359
|
+
// to `unknown` for every other non-zero exit.
|
|
360
|
+
if (busUnreachable(shown.stderr)) return inspectSystemdOffline(deps.home);
|
|
361
|
+
return unknown(`systemctl show exited ${String(shown.status)}: ${shown.stderr.trim()}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const loadState = systemdProperty(shown.stdout, "LoadState");
|
|
365
|
+
const activeState = systemdProperty(shown.stdout, "ActiveState");
|
|
366
|
+
const fragmentPath = systemdProperty(shown.stdout, "FragmentPath");
|
|
367
|
+
const needReload = systemdProperty(shown.stdout, "NeedDaemonReload");
|
|
368
|
+
if (loadState === null || activeState === null || needReload === null) {
|
|
369
|
+
return unknown("systemctl show did not report the properties it was asked for");
|
|
370
|
+
}
|
|
371
|
+
if (needReload === "yes") {
|
|
372
|
+
return unknown("systemd has a stale definition loaded; it needs daemon-reload");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const registration: "present" | "absent" =
|
|
376
|
+
loadState === "not-found" && activeState === "inactive" && !fragmentPath ? "absent" : "present";
|
|
377
|
+
|
|
378
|
+
if (artifactPresence(definitionPath) === "absent") {
|
|
379
|
+
return registration === "absent"
|
|
380
|
+
? { kind: "absent" }
|
|
381
|
+
: unknown("systemd knows this unit but its file is missing");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let body: string;
|
|
385
|
+
try {
|
|
386
|
+
body = readFileSync(definitionPath, "utf-8");
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return unknown(`the systemd unit exists but could not be read: ${String(error)}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
kind: "present",
|
|
393
|
+
claims: [{
|
|
394
|
+
backend: "systemd",
|
|
395
|
+
definitionPath,
|
|
396
|
+
homes: {
|
|
397
|
+
codexHome: unitEnvValue(body, "CODEX_HOME"),
|
|
398
|
+
opencodexHome: unitEnvValue(body, "OPENCODEX_HOME"),
|
|
399
|
+
},
|
|
400
|
+
registration,
|
|
401
|
+
}],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Windows: walk the scheduled-task definition chain and report the homes it names.
|
|
407
|
+
*
|
|
408
|
+
* The chain is not one file: the task XML names only the launcher, the launcher
|
|
409
|
+
* (VBS) names only the batch wrapper, and the homes live in the wrapper's
|
|
410
|
+
* `set "CODEX_HOME=..."` / `set "OPENCODEX_HOME=..."` lines. Parsing the XML
|
|
411
|
+
* and stopping would find no homes and read that as agreement, so the walk goes
|
|
412
|
+
* all the way to the wrapper.
|
|
413
|
+
*
|
|
414
|
+
* A `set` line is OMITTED by `buildWindowsServiceScript` when the value was
|
|
415
|
+
* unset at install time (windowsBatchSet returns null for empty values), so a
|
|
416
|
+
* missing home stays `null` — the same contract the launchd/systemd probes use —
|
|
417
|
+
* and a definition that names no homes cannot be mistaken for agreement.
|
|
418
|
+
*
|
|
419
|
+
* Registration is answered by bounded `schtasks` queries so a definition staged
|
|
420
|
+
* on disk but never registered is still visible (the interrupted-install case).
|
|
421
|
+
* Every failure to ask is `unknown`, never absence.
|
|
422
|
+
*/
|
|
423
|
+
function windowsTaskName(): string {
|
|
424
|
+
return "opencodex-proxy";
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function windowsConfigDirPath(deps: { home: string; configDir?: string }): string {
|
|
428
|
+
if (deps.configDir) return deps.configDir;
|
|
429
|
+
return join(deps.home, ".opencodex");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Decode the XML entities emitted by the service-definition writers. */
|
|
433
|
+
function decodeXmlEntities(value: string): string {
|
|
434
|
+
return value
|
|
435
|
+
.replace(/"/g, '"')
|
|
436
|
+
.replace(/</g, "<")
|
|
437
|
+
.replace(/>/g, ">")
|
|
438
|
+
.replace(/'/g, "'")
|
|
439
|
+
.replace(/&/g, "&");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Pull the launcher path out of the task XML `<Arguments>` element. */
|
|
443
|
+
function windowsTaskArguments(xml: string): string | null {
|
|
444
|
+
const match = /<Arguments[^>]*>\s*([^<]*?)\s*<\/Arguments>/i.exec(xml);
|
|
445
|
+
return match ? decodeXmlEntities(match[1]!.trim()) : null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Pull the wrapper path out of a VBS `shell.Run` line.
|
|
450
|
+
*
|
|
451
|
+
* `buildWindowsLauncherVbs` escapes a `"` inside a VBS string literal by
|
|
452
|
+
* doubling it, so a wrapper `C:\...\opencodex-service.cmd` is emitted as
|
|
453
|
+
* `shell.Run """C:\...\opencodex-service.cmd""", 0, True`.
|
|
454
|
+
*/
|
|
455
|
+
function vbsWrappedCommand(body: string): string | null {
|
|
456
|
+
const match = /\.Run\s+"""([^"]*)"""/.exec(body);
|
|
457
|
+
if (match) {
|
|
458
|
+
const unwrapped = match[1]!.trim();
|
|
459
|
+
if (unwrapped.length > 0) return unwrapped;
|
|
460
|
+
}
|
|
461
|
+
const plain = /\.Run\s+"([^"]+)"/.exec(body);
|
|
462
|
+
return plain ? plain[1]!.trim() : null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Pull one `set "NAME=value"` out of a batch wrapper. */
|
|
466
|
+
function batchSetValue(body: string, name: string): string | null {
|
|
467
|
+
const match = new RegExp(`^\\s*set\\s+"${name}=([^"]*)"\\s*$`, "im").exec(body);
|
|
468
|
+
return match ? match[1]!.trim() : null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Resolve generated batch env indirection before comparing homes. */
|
|
472
|
+
function decodeBatchPathValue(
|
|
473
|
+
value: string,
|
|
474
|
+
env: Record<string, string | undefined> = process.env,
|
|
475
|
+
): string {
|
|
476
|
+
const escapedPercent = "\u0000";
|
|
477
|
+
const tokens: Record<string, string | undefined> = {
|
|
478
|
+
USERPROFILE: env.USERPROFILE,
|
|
479
|
+
APPDATA: env.APPDATA,
|
|
480
|
+
LOCALAPPDATA: env.LOCALAPPDATA,
|
|
481
|
+
SYSTEMROOT: env.SystemRoot,
|
|
482
|
+
};
|
|
483
|
+
return value
|
|
484
|
+
.replace(/%%/g, escapedPercent)
|
|
485
|
+
.replace(/%([A-Za-z][A-Za-z0-9_]*)%/g, (whole, name: string) => {
|
|
486
|
+
const resolved = tokens[name.toUpperCase()];
|
|
487
|
+
return resolved === undefined ? whole : resolved;
|
|
488
|
+
})
|
|
489
|
+
.replaceAll(escapedPercent, "%");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Validate the generated wrapper before interpreting omitted optional homes. */
|
|
493
|
+
function wrapperLooksGenerated(body: string): boolean {
|
|
494
|
+
return /:loop\s*[\s\S]*^"%OCX_BUN%" "%OCX_CLI%" start\b[^\r\n]*$/im.test(body);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function normalizeWindowsPath(value: string): string {
|
|
498
|
+
return win32Path.normalize(value.replace(/\//g, "\\")).replace(/[\\]+$/, "").toLowerCase();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** True only when a definition-provided path remains inside the effective OPENCODEX_HOME. */
|
|
502
|
+
function windowsPathInsideConfigDir(candidate: string, configDir: string): boolean {
|
|
503
|
+
const root = normalizeWindowsPath(configDir);
|
|
504
|
+
const path = normalizeWindowsPath(candidate);
|
|
505
|
+
const relative = win32Path.relative(root, path);
|
|
506
|
+
return relative === "" || (relative !== ".." && !relative.startsWith("..\\") && !win32Path.isAbsolute(relative));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Parse the first CSV field emitted by schtasks `/fo CSV`. */
|
|
510
|
+
function csvFirstField(line: string): string {
|
|
511
|
+
const trimmed = line.trim();
|
|
512
|
+
if (!trimmed.startsWith('"')) return (trimmed.split(",", 1)[0] ?? "").trim();
|
|
513
|
+
let value = "";
|
|
514
|
+
for (let i = 1; i < trimmed.length; i += 1) {
|
|
515
|
+
const ch = trimmed[i]!;
|
|
516
|
+
if (ch !== '"') {
|
|
517
|
+
value += ch;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (trimmed[i + 1] === '"') {
|
|
521
|
+
value += '"';
|
|
522
|
+
i += 1;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function windowsTaskListContains(body: string, taskName: string): boolean {
|
|
531
|
+
const target = taskName.toLowerCase();
|
|
532
|
+
return body.split(/\r?\n/).some(line => {
|
|
533
|
+
const field = csvFirstField(line).replace(/\//g, "\\").replace(/^\\+/, "");
|
|
534
|
+
return field.toLowerCase() === target;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** English hosts provide a decisive fast path; other locales fall back to a full listing. */
|
|
539
|
+
const SCHTASKS_TASK_NOT_FOUND_EN = /cannot find the file specified/i;
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Registration state of the scheduled task.
|
|
543
|
+
*
|
|
544
|
+
* The `/xml` query gives the authoritative registered definition. A nonzero
|
|
545
|
+
* result is locale-dependent. English's task-not-found message is decisive; all
|
|
546
|
+
* other nonzero responses use a bounded full listing as the locale-neutral
|
|
547
|
+
* fallback, and only a successful list without our task proves absence.
|
|
548
|
+
*/
|
|
549
|
+
function probeWindowsTaskRegistration(
|
|
550
|
+
deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "windowsLocale">,
|
|
551
|
+
): {
|
|
552
|
+
registered: "present" | "absent" | "unknown";
|
|
553
|
+
registeredXml: string;
|
|
554
|
+
} {
|
|
555
|
+
let schtasks: string;
|
|
556
|
+
try {
|
|
557
|
+
schtasks = resolveTrustedWindowsSchtasksExe();
|
|
558
|
+
} catch {
|
|
559
|
+
return { registered: "unknown", registeredXml: "" };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const queried = deps.runRaw(schtasks, ["/query", "/tn", windowsTaskName(), "/xml"]);
|
|
563
|
+
if (queried.spawnFailed || queried.timedOut) return { registered: "unknown", registeredXml: "" };
|
|
564
|
+
if (queried.status === 0) {
|
|
565
|
+
const registeredXml = decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })
|
|
566
|
+
|| decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale });
|
|
567
|
+
return registeredXml
|
|
568
|
+
? { registered: "present", registeredXml }
|
|
569
|
+
: { registered: "unknown", registeredXml: "" };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const queryText = `${decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })}\n${decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale })}`;
|
|
573
|
+
if (queried.status !== null && SCHTASKS_TASK_NOT_FOUND_EN.test(queryText)) {
|
|
574
|
+
return { registered: "absent", registeredXml: "" };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const listed = deps.runRaw(schtasks, ["/query", "/fo", "CSV", "/nh"]);
|
|
578
|
+
if (listed.spawnFailed || listed.timedOut || listed.status !== 0) {
|
|
579
|
+
return { registered: "unknown", registeredXml: "" };
|
|
580
|
+
}
|
|
581
|
+
const listing = decodeWindowsTextBytes(listed.stdout, { locale: deps.windowsLocale })
|
|
582
|
+
|| decodeWindowsTextBytes(listed.stderr, { locale: deps.windowsLocale });
|
|
583
|
+
return windowsTaskListContains(listing, windowsTaskName())
|
|
584
|
+
? { registered: "unknown", registeredXml: "" }
|
|
585
|
+
: { registered: "absent", registeredXml: "" };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
type WinswRegistration = "present" | "absent" | "unknown";
|
|
589
|
+
|
|
590
|
+
/** Query WinSW registration through trusted System32 sc.exe; never execute the user-writable WinSW binary. */
|
|
591
|
+
function probeWinswRegistration(
|
|
592
|
+
deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "winswStatus">,
|
|
593
|
+
): WinswRegistration {
|
|
594
|
+
if (deps.winswStatus) {
|
|
595
|
+
const injected = deps.winswStatus();
|
|
596
|
+
if (injected === "started" || injected === "stopped") return "present";
|
|
597
|
+
if (injected === "nonexistent") return "absent";
|
|
598
|
+
return "unknown";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let sc: string;
|
|
602
|
+
try {
|
|
603
|
+
sc = join(resolveTrustedWindowsSystemDirectory(), "sc.exe");
|
|
604
|
+
if (artifactPresence(sc) !== "present") return "unknown";
|
|
605
|
+
} catch {
|
|
606
|
+
return "unknown";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const queried = deps.runRaw(sc, ["query", WINSW_SERVICE_ID]);
|
|
610
|
+
if (queried.spawnFailed || queried.timedOut) return "unknown";
|
|
611
|
+
if (queried.status === 0) return "present";
|
|
612
|
+
|
|
613
|
+
// ERROR_SERVICE_DOES_NOT_EXIST (1060) is locale-invariant. Search raw byte
|
|
614
|
+
// text so localized OEM output cannot affect the numeric classification.
|
|
615
|
+
const text = `${queried.stdout.toString("latin1")}\n${queried.stderr.toString("latin1")}`;
|
|
616
|
+
return /\b1060\b/.test(text) ? "absent" : "unknown";
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function inspectWindows(
|
|
620
|
+
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
621
|
+
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
|
|
622
|
+
): ServiceManagerInstallation {
|
|
623
|
+
const configDir = windowsConfigDirPath(deps);
|
|
624
|
+
const taskXmlPath = join(configDir, "opencodex-service-task.xml");
|
|
625
|
+
const task = artifactPresence(taskXmlPath);
|
|
626
|
+
const winsw = walkWinswChain(deps);
|
|
627
|
+
const winswInstalled = winsw.kind === "present" && winsw.claims[0].registration === "present";
|
|
628
|
+
const winswStaged = winsw.kind === "present" && winsw.claims[0].registration === "absent";
|
|
629
|
+
|
|
630
|
+
let xml = "";
|
|
631
|
+
if (task !== "absent") {
|
|
632
|
+
try {
|
|
633
|
+
xml = decodeWindowsTextBytes(readFileSync(taskXmlPath), { locale: deps.windowsLocale });
|
|
634
|
+
} catch (error) {
|
|
635
|
+
return unknown(`the scheduled-task XML exists but could not be read: ${String(error)}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const registration = probeWindowsTaskRegistration(deps);
|
|
640
|
+
if (registration.registered === "unknown") {
|
|
641
|
+
return unknown("Task Scheduler could not be asked whether opencodex-proxy is registered");
|
|
642
|
+
}
|
|
643
|
+
const schedulerRegistered = registration.registered === "present";
|
|
644
|
+
|
|
645
|
+
// Two live registrations are a conflict even if the staging copy of the task
|
|
646
|
+
// XML disappeared. The authoritative `/query /xml` definition is sufficient
|
|
647
|
+
// to walk the scheduler chain without inventing homes.
|
|
648
|
+
if (winswInstalled && schedulerRegistered) {
|
|
649
|
+
if (!registration.registeredXml.trim()) {
|
|
650
|
+
return unknown("Task Scheduler is registered but its definition XML could not be read");
|
|
651
|
+
}
|
|
652
|
+
const registeredWalk = walkWindowsChain(deps, registration.registeredXml, taskXmlPath);
|
|
653
|
+
if (registeredWalk.kind !== "present") return registeredWalk;
|
|
654
|
+
return {
|
|
655
|
+
kind: "conflict",
|
|
656
|
+
claims: [
|
|
657
|
+
winsw.claims[0],
|
|
658
|
+
{ ...registeredWalk.claims[0], registration: "present" },
|
|
659
|
+
],
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (winswInstalled) {
|
|
664
|
+
// A staged scheduler definition beside a registered WinSW service is an
|
|
665
|
+
// interrupted backend switch, not proof that WinSW alone owns the machine.
|
|
666
|
+
if (task !== "absent") {
|
|
667
|
+
return unknown("a scheduled-task definition is staged while the native WinSW service is registered");
|
|
668
|
+
}
|
|
669
|
+
return winsw;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (winsw.kind === "unknown") return winsw;
|
|
673
|
+
|
|
674
|
+
// Staged-but-unregistered WinSW remains evidence. If Scheduler is also live
|
|
675
|
+
// or staged, neither half-finished backend switch can be chosen unattended.
|
|
676
|
+
if (winswStaged && (schedulerRegistered || task !== "absent")) {
|
|
677
|
+
return unknown("native WinSW and Task Scheduler definitions overlap during an incomplete backend switch");
|
|
678
|
+
}
|
|
679
|
+
if (winswStaged && task === "absent" && registration.registered === "absent") {
|
|
680
|
+
return winsw;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (task === "absent") {
|
|
684
|
+
return schedulerRegistered
|
|
685
|
+
? unknown("Task Scheduler holds opencodex-proxy but its task XML is missing")
|
|
686
|
+
: { kind: "absent" };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const staged = walkWindowsChain(deps, xml, taskXmlPath);
|
|
690
|
+
if (staged.kind !== "present") return staged;
|
|
691
|
+
const stagedClaim = staged.claims[0];
|
|
692
|
+
|
|
693
|
+
if (schedulerRegistered) {
|
|
694
|
+
if (!registration.registeredXml.trim()) {
|
|
695
|
+
return unknown("Task Scheduler is registered but its definition XML could not be read");
|
|
696
|
+
}
|
|
697
|
+
const registeredWalk = walkWindowsChain(deps, registration.registeredXml, taskXmlPath);
|
|
698
|
+
if (registeredWalk.kind !== "present") return registeredWalk;
|
|
699
|
+
const registeredClaim = registeredWalk.claims[0];
|
|
700
|
+
if (!homesEqual(registeredClaim.homes, stagedClaim.homes)) {
|
|
701
|
+
return unknown("the registered scheduled task names different homes than the staged task definition");
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
kind: "present",
|
|
707
|
+
claims: [{
|
|
708
|
+
...stagedClaim,
|
|
709
|
+
registration: schedulerRegistered ? "present" : "absent",
|
|
710
|
+
}],
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Compare two home pairs with Windows path normalization (case, slashes, trailing separators). */
|
|
715
|
+
function homesEqual(
|
|
716
|
+
a: { codexHome: string | null; opencodexHome: string | null },
|
|
717
|
+
b: { codexHome: string | null; opencodexHome: string | null },
|
|
718
|
+
): boolean {
|
|
719
|
+
const norm = (v: string | null): string | null => {
|
|
720
|
+
if (v === null) return null;
|
|
721
|
+
return v.replace(/[\\/]+$/, "").replace(/\//g, "\\").toLowerCase();
|
|
722
|
+
};
|
|
723
|
+
return norm(a.codexHome) === norm(b.codexHome) && norm(a.opencodexHome) === norm(b.opencodexHome);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Walk one scheduled-task definition (staged or registered XML) down to the
|
|
728
|
+
* generated batch wrapper and extract the homes it names. Definition-provided
|
|
729
|
+
* paths are followed only inside the effective OPENCODEX_HOME, preventing a
|
|
730
|
+
* foreign task from turning this ownership probe into an arbitrary local/UNC
|
|
731
|
+
* file read while preserving interrupted-reinstall diagnostics within the
|
|
732
|
+
* generated service-asset directory.
|
|
733
|
+
*/
|
|
734
|
+
function walkWindowsChain(
|
|
735
|
+
deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale">,
|
|
736
|
+
xml: string,
|
|
737
|
+
definitionPath: string,
|
|
738
|
+
): ServiceManagerInstallation {
|
|
739
|
+
const configDir = windowsConfigDirPath(deps);
|
|
740
|
+
|
|
741
|
+
const launcherArg = windowsTaskArguments(xml);
|
|
742
|
+
if (!launcherArg) {
|
|
743
|
+
return unknown("the scheduled-task XML names no launcher to run");
|
|
744
|
+
}
|
|
745
|
+
const launcherPath = /"([^"]+)"/.exec(launcherArg)?.[1];
|
|
746
|
+
if (!launcherPath) {
|
|
747
|
+
return unknown("the scheduled-task XML launcher argument is not a quoted path");
|
|
748
|
+
}
|
|
749
|
+
if (!windowsPathInsideConfigDir(launcherPath, configDir)) {
|
|
750
|
+
return unknown(`the scheduled-task XML names ${launcherPath}, outside the expected launcher directory ${configDir}`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const launcher = artifactPresence(launcherPath);
|
|
754
|
+
if (launcher === "absent") {
|
|
755
|
+
return unknown(`the scheduled-task launcher is missing: ${launcherPath}`);
|
|
756
|
+
}
|
|
757
|
+
let launcherBody: string;
|
|
758
|
+
try {
|
|
759
|
+
launcherBody = decodeWindowsTextBytes(readFileSync(launcherPath), { locale: deps.windowsLocale });
|
|
760
|
+
} catch (error) {
|
|
761
|
+
return unknown(`the scheduled-task launcher could not be read: ${String(error)}`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const wrapperPath = vbsWrappedCommand(launcherBody);
|
|
765
|
+
if (!wrapperPath) {
|
|
766
|
+
return unknown(`the launcher ${launcherPath} names no wrapper to run`);
|
|
767
|
+
}
|
|
768
|
+
if (!windowsPathInsideConfigDir(wrapperPath, configDir)) {
|
|
769
|
+
return unknown(`the scheduled-task launcher names ${wrapperPath}, outside the expected wrapper directory ${configDir}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const wrapper = artifactPresence(wrapperPath);
|
|
773
|
+
if (wrapper === "absent") {
|
|
774
|
+
return unknown(`the launcher wrapper is missing: ${wrapperPath}`);
|
|
775
|
+
}
|
|
776
|
+
let wrapperBody: string;
|
|
777
|
+
try {
|
|
778
|
+
wrapperBody = decodeWindowsTextBytes(readFileSync(wrapperPath), { locale: deps.windowsLocale });
|
|
779
|
+
} catch (error) {
|
|
780
|
+
return unknown(`the launcher wrapper could not be read: ${String(error)}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (!wrapperLooksGenerated(wrapperBody)) {
|
|
784
|
+
return unknown(`the launcher wrapper does not look like a generated opencodex service wrapper: ${wrapperPath}`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const rawCodexHome = batchSetValue(wrapperBody, "CODEX_HOME");
|
|
788
|
+
const rawOpencodexHome = batchSetValue(wrapperBody, "OPENCODEX_HOME");
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
kind: "present",
|
|
792
|
+
claims: [{
|
|
793
|
+
backend: "scheduler",
|
|
794
|
+
definitionPath,
|
|
795
|
+
homes: {
|
|
796
|
+
codexHome: rawCodexHome === null ? null : decodeBatchPathValue(rawCodexHome),
|
|
797
|
+
opencodexHome: rawOpencodexHome === null ? null : decodeBatchPathValue(rawOpencodexHome),
|
|
798
|
+
},
|
|
799
|
+
registration: "absent",
|
|
800
|
+
}],
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Walk the WinSW native-backend definition. SCM registration is queried via a
|
|
806
|
+
* trusted, bounded `sc.exe query`; the WinSW executable itself is never run by
|
|
807
|
+
* this read-only ownership probe.
|
|
808
|
+
*/
|
|
809
|
+
function walkWinswChain(
|
|
810
|
+
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
811
|
+
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
|
|
812
|
+
): ServiceManagerInstallation {
|
|
813
|
+
const configDir = windowsConfigDirPath(deps);
|
|
814
|
+
const exePath = join(configDir, "winsw", `${WINSW_SERVICE_ID}.exe`);
|
|
815
|
+
const xmlPath = join(configDir, "winsw", `${WINSW_SERVICE_ID}.xml`);
|
|
816
|
+
const xml = artifactPresence(xmlPath);
|
|
817
|
+
const exe = artifactPresence(exePath);
|
|
818
|
+
const registration = probeWinswRegistration(deps);
|
|
819
|
+
|
|
820
|
+
if (xml === "absent" && exe === "absent" && registration === "absent") return { kind: "absent" };
|
|
821
|
+
// A query we could not ask is a question about a service that cannot exist: WinSW is an
|
|
822
|
+
// optional backend, and with neither its XML nor its exe on disk there is nothing for a
|
|
823
|
+
// registration to belong to. Fencing here on an `sc.exe` timeout is one of the two
|
|
824
|
+
// triggers behind #2108, where a scheduler-only install answers 503 until `ocx restart`.
|
|
825
|
+
//
|
|
826
|
+
// The disk outranks the unaskable query only when BOTH assets are gone. Either one
|
|
827
|
+
// present means a real install may be there and the old `unknown` still holds.
|
|
828
|
+
if (registration === "unknown" && xml === "absent" && exe === "absent") {
|
|
829
|
+
return { kind: "absent" };
|
|
830
|
+
}
|
|
831
|
+
if (registration === "unknown") {
|
|
832
|
+
return unknown("the native WinSW service registration could not be verified");
|
|
833
|
+
}
|
|
834
|
+
if (xml === "absent" || exe === "absent") {
|
|
835
|
+
return unknown("the native WinSW service registration could not be verified");
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
let body: string;
|
|
839
|
+
try {
|
|
840
|
+
body = decodeWindowsTextBytes(readFileSync(xmlPath), { locale: deps.windowsLocale });
|
|
841
|
+
} catch (error) {
|
|
842
|
+
return unknown(`the WinSW XML could not be read: ${String(error)}`);
|
|
843
|
+
}
|
|
844
|
+
if (!winswXmlLooksGenerated(body)) {
|
|
845
|
+
return unknown(`the WinSW XML does not look like a generated opencodex service definition: ${xmlPath}`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const envValue = (name: string): string | null => {
|
|
849
|
+
const tag = new RegExp(`<env\\b[^>]*\\bname=["']${name}["'][^>]*>`, "i").exec(body);
|
|
850
|
+
if (!tag) return null;
|
|
851
|
+
const value = /value=(["'])(.*?)\1/i.exec(tag[0]);
|
|
852
|
+
return value ? decodeXmlEntities(value[2]!) : null;
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
kind: "present",
|
|
857
|
+
claims: [{
|
|
858
|
+
backend: "winsw",
|
|
859
|
+
definitionPath: exePath,
|
|
860
|
+
homes: {
|
|
861
|
+
codexHome: envValue("CODEX_HOME"),
|
|
862
|
+
opencodexHome: envValue("OPENCODEX_HOME"),
|
|
863
|
+
},
|
|
864
|
+
registration,
|
|
865
|
+
}],
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** The generated WinSW XML embeds the SCM id and a `start --port` invocation. */
|
|
870
|
+
function winswXmlLooksGenerated(body: string): boolean {
|
|
871
|
+
return /<id>\s*opencodex-proxy-native\s*<\/id>/i.test(body)
|
|
872
|
+
&& /<arguments>.*?start\s+--port\b/i.test(body);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function inspectServiceManagerInstallation(deps: ProbeDeps = {}): ServiceManagerInstallation {
|
|
876
|
+
const platform = deps.platform ?? process.platform;
|
|
877
|
+
const run = deps.run ?? defaultProbeRunner;
|
|
878
|
+
const runRaw = deps.runRaw ?? defaultRawProbeRunner;
|
|
879
|
+
const home = deps.home ?? homedir();
|
|
880
|
+
if (platform === "darwin") return inspectLaunchd({ run, uid: deps.uid ?? process.getuid?.() ?? 0, home });
|
|
881
|
+
if (platform === "linux") return inspectSystemd({ run, home });
|
|
882
|
+
if (platform === "win32") {
|
|
883
|
+
return inspectWindows({
|
|
884
|
+
runRaw,
|
|
885
|
+
home,
|
|
886
|
+
configDir: deps.configDir,
|
|
887
|
+
winswStatus: deps.winswStatus,
|
|
888
|
+
windowsLocale: deps.windowsLocale,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
return unknown(`no service manager probe for platform ${platform}`);
|
|
892
|
+
}
|