@iislee/opencodex 2.11.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +622 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "What is on disk, and did we put it there?"
|
|
3
|
+
*
|
|
4
|
+
* The classifier is deliberately ordered, and the order is load-bearing: an
|
|
5
|
+
* unreadable file can never be reported as absent, and an edit to a fragment
|
|
6
|
+
* we own can never be reported as ordinary drift. Getting that wrong would let
|
|
7
|
+
* `disable` delete a user's own edits.
|
|
8
|
+
*
|
|
9
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/021 §3.
|
|
10
|
+
*/
|
|
11
|
+
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
12
|
+
import type { OcxConfig } from "../types";
|
|
13
|
+
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
14
|
+
import { SNAPSHOT_RETENTION } from "./journal";
|
|
15
|
+
import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
|
|
16
|
+
import {
|
|
17
|
+
protectedContributionFingerprint,
|
|
18
|
+
refreshablePathsOf,
|
|
19
|
+
semanticProtectedContributionFingerprint,
|
|
20
|
+
validRefreshablePaths,
|
|
21
|
+
} from "./ownership-policy";
|
|
22
|
+
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
23
|
+
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
24
|
+
|
|
25
|
+
export type IntegrationState = "absent" | "current" | "stale" | "conflict" | "unsafe";
|
|
26
|
+
export type StateReason =
|
|
27
|
+
| "unparseable"
|
|
28
|
+
| "not-regular-file"
|
|
29
|
+
| "foreign-edit"
|
|
30
|
+
| "unowned-key"
|
|
31
|
+
/** A container we would have to write through holds a non-object value. */
|
|
32
|
+
| "blocked-container"
|
|
33
|
+
/** A path selector we cannot resolve, e.g. a relative OPENCLAW_CONFIG_PATH. */
|
|
34
|
+
| "unresolvable-path";
|
|
35
|
+
|
|
36
|
+
export interface IntegrationStatus {
|
|
37
|
+
clientId: IntegrationClientId;
|
|
38
|
+
state: IntegrationState;
|
|
39
|
+
installed: boolean;
|
|
40
|
+
configPath: string;
|
|
41
|
+
appliedAt?: string;
|
|
42
|
+
lastOpId?: string;
|
|
43
|
+
reason?: StateReason;
|
|
44
|
+
/** Snapshot files retained for this client; -1 when they cannot be inspected. */
|
|
45
|
+
snapshotCount: number;
|
|
46
|
+
/** Pruning is behind, so older (possibly credential-bearing) snapshots remain. */
|
|
47
|
+
retentionDegraded: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readPath(doc: unknown, path: readonly string[]): unknown {
|
|
51
|
+
let cursor: unknown = doc;
|
|
52
|
+
for (const key of path) {
|
|
53
|
+
if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) return undefined;
|
|
54
|
+
cursor = (cursor as Record<string, unknown>)[key];
|
|
55
|
+
if (cursor === undefined) return undefined;
|
|
56
|
+
}
|
|
57
|
+
return cursor;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Does the document carry any fragment we would write? */
|
|
61
|
+
export function hasOurFragments(doc: unknown, contribution: ManagedContribution): boolean {
|
|
62
|
+
return contribution.fragments.some(fragment => readPath(doc, fragment.path) !== undefined);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A container on one of our fragment paths that exists but is NOT an object.
|
|
67
|
+
*
|
|
68
|
+
* `setPath` replaces such a value with `{}` on the way to writing our leaf, so
|
|
69
|
+
* a user whose config held `providers: ["something"]` — legal in their schema,
|
|
70
|
+
* just not ours — lost it to an apply that reported success. The classifier
|
|
71
|
+
* called that document `absent` because our leaf was missing, which authorized
|
|
72
|
+
* the write. Detecting it here turns a silent overwrite into a refusal the
|
|
73
|
+
* user can act on; the snapshot exists, but "we backed up the thing we should
|
|
74
|
+
* not have destroyed" is not the promise this feature makes.
|
|
75
|
+
*/
|
|
76
|
+
export function blockedContainerPath(
|
|
77
|
+
doc: unknown,
|
|
78
|
+
contribution: ManagedContribution,
|
|
79
|
+
): readonly string[] | null {
|
|
80
|
+
for (const fragment of contribution.fragments) {
|
|
81
|
+
let cursor: unknown = doc;
|
|
82
|
+
for (let depth = 0; depth < fragment.path.length - 1; depth += 1) {
|
|
83
|
+
const key = fragment.path[depth]!;
|
|
84
|
+
/*
|
|
85
|
+
* ONLY `undefined` means absent. A missing file parses as `{}`, so an
|
|
86
|
+
* absent prefix reads `undefined` — but a parsed `null` is a value the
|
|
87
|
+
* user's file actually contains, and treating it as absent let a
|
|
88
|
+
* document that is literally `null` be replaced wholesale by a
|
|
89
|
+
* "successful" apply.
|
|
90
|
+
*/
|
|
91
|
+
if (cursor === undefined) break;
|
|
92
|
+
// `typeof null === "object"`, so null has to be named explicitly or it
|
|
93
|
+
// walks straight into the dereference below.
|
|
94
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
95
|
+
return fragment.path.slice(0, depth);
|
|
96
|
+
}
|
|
97
|
+
const next = (cursor as Record<string, unknown>)[key];
|
|
98
|
+
if (next === undefined) break;
|
|
99
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) {
|
|
100
|
+
return fragment.path.slice(0, depth + 1);
|
|
101
|
+
}
|
|
102
|
+
cursor = next;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fingerprint the recorded fragments as they appear in the document now.
|
|
110
|
+
*
|
|
111
|
+
* The record intentionally names every path we own. Comparing just those
|
|
112
|
+
* values lets another integration or a user add a sibling without blocking a
|
|
113
|
+
* later refresh, while a change inside our block still fails closed.
|
|
114
|
+
*/
|
|
115
|
+
function recordedContribution(
|
|
116
|
+
doc: unknown,
|
|
117
|
+
record: OwnershipRecord,
|
|
118
|
+
): ManagedContribution | null {
|
|
119
|
+
if (
|
|
120
|
+
!Array.isArray(record.fragmentPaths)
|
|
121
|
+
|| record.fragmentPaths.length === 0
|
|
122
|
+
|| !record.fragmentPaths.every(path => (
|
|
123
|
+
Array.isArray(path)
|
|
124
|
+
&& path.length > 0
|
|
125
|
+
&& path.every(key => typeof key === "string")
|
|
126
|
+
))
|
|
127
|
+
) return null;
|
|
128
|
+
const fragments = [];
|
|
129
|
+
for (const path of record.fragmentPaths) {
|
|
130
|
+
const value = readPath(doc, path);
|
|
131
|
+
if (value === undefined) return null;
|
|
132
|
+
fragments.push({ path, value });
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
clientId: record.clientId,
|
|
136
|
+
fragments,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Prove that every protected field still matches what OpenCodex wrote.
|
|
142
|
+
*
|
|
143
|
+
* New records carry an operation-scoped protected fingerprint and the exact
|
|
144
|
+
* paths excluded from it. Legacy records can recover only when the desired
|
|
145
|
+
* contribution has not moved since apply; otherwise catalog drift and a
|
|
146
|
+
* foreign edit are indistinguishable, so the classifier keeps failing closed.
|
|
147
|
+
*/
|
|
148
|
+
function recordedBlockIsOwned(
|
|
149
|
+
doc: unknown,
|
|
150
|
+
record: OwnershipRecord,
|
|
151
|
+
desired: ManagedContribution,
|
|
152
|
+
): boolean {
|
|
153
|
+
const observed = recordedContribution(doc, record);
|
|
154
|
+
if (!observed) return false;
|
|
155
|
+
if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
|
|
156
|
+
|
|
157
|
+
const observedSemanticFingerprint = fingerprint(semanticContribution(observed));
|
|
158
|
+
if (
|
|
159
|
+
typeof record.semanticBlockFingerprint === "string"
|
|
160
|
+
&& observedSemanticFingerprint === record.semanticBlockFingerprint
|
|
161
|
+
) return true;
|
|
162
|
+
|
|
163
|
+
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
164
|
+
if (
|
|
165
|
+
desiredFingerprint === record.blockFingerprint
|
|
166
|
+
&& observedSemanticFingerprint === fingerprint(semanticContribution(desired))
|
|
167
|
+
) return true;
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
typeof record.protectedBlockFingerprint === "string"
|
|
171
|
+
&& validRefreshablePaths(observed, record.refreshablePaths)
|
|
172
|
+
&& record.refreshablePaths.length > 0
|
|
173
|
+
) {
|
|
174
|
+
const observedProtectedFingerprint = protectedContributionFingerprint(
|
|
175
|
+
observed,
|
|
176
|
+
record.refreshablePaths,
|
|
177
|
+
);
|
|
178
|
+
if (observedProtectedFingerprint === record.protectedBlockFingerprint) return true;
|
|
179
|
+
|
|
180
|
+
const observedSemanticProtectedFingerprint = semanticProtectedContributionFingerprint(
|
|
181
|
+
observed,
|
|
182
|
+
record.refreshablePaths,
|
|
183
|
+
);
|
|
184
|
+
if (
|
|
185
|
+
typeof record.semanticProtectedBlockFingerprint === "string"
|
|
186
|
+
&& observedSemanticProtectedFingerprint === record.semanticProtectedBlockFingerprint
|
|
187
|
+
) return true;
|
|
188
|
+
|
|
189
|
+
return protectedContributionFingerprint(desired, record.refreshablePaths)
|
|
190
|
+
=== record.protectedBlockFingerprint
|
|
191
|
+
&& observedSemanticProtectedFingerprint
|
|
192
|
+
=== semanticProtectedContributionFingerprint(desired, record.refreshablePaths);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (desiredFingerprint !== record.blockFingerprint) return false;
|
|
196
|
+
const legacyPaths = refreshablePathsOf(desired);
|
|
197
|
+
return legacyPaths.length > 0
|
|
198
|
+
&& semanticProtectedContributionFingerprint(observed, legacyPaths)
|
|
199
|
+
=== semanticProtectedContributionFingerprint(desired, legacyPaths);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The two-axis rule: the recorded bytes or fragments prove nobody changed
|
|
204
|
+
* what we may rewrite, and the contribution hash proves our catalog has not
|
|
205
|
+
* moved on. Three classes of client (revising the unconditional whole-file
|
|
206
|
+
* rule of devlog 260802_client_toggle_api/021 §3 for json — #1631):
|
|
207
|
+
* registry-declared source-preserving YAML clients are fragment-scoped because
|
|
208
|
+
* their writers patch only the owned leaf, so the whole-file check is skipped;
|
|
209
|
+
* strict-json clients keep the whole-file check but downgrade a drift with
|
|
210
|
+
* intact owned fragments to `stale`, because a rewrite there can lose only
|
|
211
|
+
* formatting (comments cannot parse, non-round-tripping numbers are refused
|
|
212
|
+
* by the serializer); every comment-capable whole-document serializer (yaml,
|
|
213
|
+
* json5, toml) retains the whole-file fingerprint guard as a hard conflict.
|
|
214
|
+
*/
|
|
215
|
+
export function classifyIntegration(input: {
|
|
216
|
+
fileText: string | null;
|
|
217
|
+
fileIsRegular: boolean;
|
|
218
|
+
parsed: unknown | typeof PARSE_FAILED;
|
|
219
|
+
record: OwnershipRecord | null;
|
|
220
|
+
contribution: ManagedContribution;
|
|
221
|
+
/**
|
|
222
|
+
* The file being classified. A record only describes the file it was written
|
|
223
|
+
* for, so this is compared against `record.configPath` before any
|
|
224
|
+
* fingerprint is trusted.
|
|
225
|
+
*/
|
|
226
|
+
configPath?: string;
|
|
227
|
+
clientId?: IntegrationClientId;
|
|
228
|
+
}): { state: IntegrationState; reason?: StateReason } {
|
|
229
|
+
if (input.fileText !== null && !input.fileIsRegular) {
|
|
230
|
+
return { state: "unsafe", reason: "not-regular-file" };
|
|
231
|
+
}
|
|
232
|
+
if (input.parsed === PARSE_FAILED) return { state: "unsafe", reason: "unparseable" };
|
|
233
|
+
/*
|
|
234
|
+
* Checked BEFORE `absent`: our leaf is missing in exactly this case, so the
|
|
235
|
+
* absent branch would authorize an apply that replaces the user's value.
|
|
236
|
+
*/
|
|
237
|
+
if (blockedContainerPath(input.parsed, input.contribution)) {
|
|
238
|
+
return { state: "unsafe", reason: "blocked-container" };
|
|
239
|
+
}
|
|
240
|
+
if (!hasOurFragments(input.parsed, input.contribution)) return { state: "absent" };
|
|
241
|
+
if (!input.record) return { state: "conflict", reason: "unowned-key" };
|
|
242
|
+
/*
|
|
243
|
+
* A record proves ownership of ONE file. Change HOME, XDG_CONFIG_HOME,
|
|
244
|
+
* HERMES_HOME or KIMI_CODE_HOME and the same client resolves to a different
|
|
245
|
+
* path — whose contents may hash identically because we generate the same
|
|
246
|
+
* bytes. Trusting the fingerprint alone would let a record for path A grant
|
|
247
|
+
* `current` on path B, and the writer resolves the CURRENT path, so disable
|
|
248
|
+
* would then delete fragments from a file this record never owned.
|
|
249
|
+
*/
|
|
250
|
+
if (input.clientId !== undefined && input.record.clientId !== input.clientId) {
|
|
251
|
+
return { state: "conflict", reason: "unowned-key" };
|
|
252
|
+
}
|
|
253
|
+
if (input.configPath !== undefined && input.record.configPath !== input.configPath) {
|
|
254
|
+
return { state: "conflict", reason: "unowned-key" };
|
|
255
|
+
}
|
|
256
|
+
const clientId = input.clientId ?? input.record.clientId;
|
|
257
|
+
/*
|
|
258
|
+
* Checked BEFORE file-level drift: an edit INSIDE an owned fragment is a
|
|
259
|
+
* conflict no matter what the rest of the file looks like, so the sibling-
|
|
260
|
+
* edit exemption below can never mask it.
|
|
261
|
+
*/
|
|
262
|
+
if (!recordedBlockIsOwned(input.parsed, input.record, input.contribution)) {
|
|
263
|
+
return { state: "conflict", reason: "foreign-edit" };
|
|
264
|
+
}
|
|
265
|
+
if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
|
|
266
|
+
&& fingerprint(input.fileText ?? "") !== input.record.fileFingerprint) {
|
|
267
|
+
/*
|
|
268
|
+
* The file changed since we wrote it, but every fragment we own is still
|
|
269
|
+
* byte-for-byte what we put there — a sibling edit, not tampering. Apply
|
|
270
|
+
* rewrites the WHOLE document, so for comment-capable formats (yaml,
|
|
271
|
+
* json5, toml) it would drop comments the user wrote next to us: fail
|
|
272
|
+
* closed there. Strict JSON cannot carry comments — a commented file
|
|
273
|
+
* never reaches this branch because parsing already failed — so the only
|
|
274
|
+
* possible loss is formatting normalization: everything a rewrite would
|
|
275
|
+
* actually change (numbers that would not round-trip, duplicate members
|
|
276
|
+
* a rewrite would delete) is PARSE_FAILED in parseConfig and classifies
|
|
277
|
+
* as unsafe long before this branch, exactly like comments. Refusing
|
|
278
|
+
* forever over formatting
|
|
279
|
+
* dead-ends the integration on the user's first own config edit (#1631).
|
|
280
|
+
* Report drift instead; a re-apply merges into the parsed document as it
|
|
281
|
+
* stands and re-owns the file. This also lets disable proceed on a
|
|
282
|
+
* drifted file — removal still touches only the recorded fragment paths.
|
|
283
|
+
*/
|
|
284
|
+
if (EXPORT_CLIENTS[clientId].format !== "json") {
|
|
285
|
+
return { state: "conflict", reason: "foreign-edit" };
|
|
286
|
+
}
|
|
287
|
+
return { state: "stale" };
|
|
288
|
+
}
|
|
289
|
+
const desiredFingerprint = typeof input.record.semanticBlockFingerprint === "string"
|
|
290
|
+
? fingerprint(semanticContribution(input.contribution))
|
|
291
|
+
: fingerprint(canonicalContribution(input.contribution));
|
|
292
|
+
const recordedFingerprint = input.record.semanticBlockFingerprint ?? input.record.blockFingerprint;
|
|
293
|
+
return recordedFingerprint === desiredFingerprint
|
|
294
|
+
? { state: "current" }
|
|
295
|
+
: { state: "stale" };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface IntegrationStateInput {
|
|
299
|
+
clientId: IntegrationClientId;
|
|
300
|
+
models: readonly ExportModel[];
|
|
301
|
+
config: OcxConfig;
|
|
302
|
+
port: number;
|
|
303
|
+
env?: NodeJS.ProcessEnv;
|
|
304
|
+
home?: string;
|
|
305
|
+
/** The whole integration state store, bound to one root. */
|
|
306
|
+
store?: IntegrationStateStore;
|
|
307
|
+
io?: IntegrationIO;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function exportContextOf(input: {
|
|
311
|
+
models: readonly ExportModel[];
|
|
312
|
+
config: OcxConfig;
|
|
313
|
+
port: number;
|
|
314
|
+
}): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig } {
|
|
315
|
+
return {
|
|
316
|
+
/*
|
|
317
|
+
* Composed through the SAME helper `ocx export` uses. Interpolating the
|
|
318
|
+
* hostname by hand looked equivalent and was not: `::1` produced
|
|
319
|
+
* `http://::1:10100/v1` and `::` produced `http://:::10100/v1`, neither of
|
|
320
|
+
* which is a URL, and a `0.0.0.0` bind wrote a wildcard address no client
|
|
321
|
+
* can dial. `opencodeProxyBaseUrl` brackets IPv6 and maps wildcards to
|
|
322
|
+
* loopback, and every client we write into deserves the same answer the
|
|
323
|
+
* export command already gives.
|
|
324
|
+
*/
|
|
325
|
+
baseUrl: opencodeProxyBaseUrl(input.port, input.config.hostname),
|
|
326
|
+
models: input.models,
|
|
327
|
+
config: input.config,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let retriedThisProcess = false;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Retry pending prunes once per process for the default store, and always for
|
|
335
|
+
* an explicitly supplied one so tests stay order-independent. Never throws: a
|
|
336
|
+
* retry failure is a logged no-op, not a failed read.
|
|
337
|
+
*/
|
|
338
|
+
export function retryPendingPrunesOnce(store: IntegrationStateStore): void {
|
|
339
|
+
if (store.root === createIntegrationStateStore().root) {
|
|
340
|
+
if (retriedThisProcess) return;
|
|
341
|
+
retriedThisProcess = true;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
store.retryPendingPrunes();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(`[integrations] prune retry failed: ${String(error)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Retention is derived from what is ON DISK, not from the maintenance marker.
|
|
352
|
+
* The marker schedules retries and can itself fail to write; a promise about
|
|
353
|
+
* the user's credential-bearing backups must not depend on that.
|
|
354
|
+
*/
|
|
355
|
+
function retentionOf(
|
|
356
|
+
clientId: IntegrationClientId,
|
|
357
|
+
store: IntegrationStateStore,
|
|
358
|
+
): { snapshotCount: number; retentionDegraded: boolean } {
|
|
359
|
+
const counted = store.countSnapshots(clientId);
|
|
360
|
+
if (counted === null) {
|
|
361
|
+
// Cannot inspect: report degraded with -1 rather than a reassuring zero.
|
|
362
|
+
return { snapshotCount: -1, retentionDegraded: true };
|
|
363
|
+
}
|
|
364
|
+
const marked = store.readMaintenance().pruneFailures[clientId] !== undefined;
|
|
365
|
+
return { snapshotCount: counted, retentionDegraded: marked || counted > SNAPSHOT_RETENTION };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The ONE reader every surface uses. */
|
|
369
|
+
export function readIntegrationState(input: IntegrationStateInput): IntegrationStatus {
|
|
370
|
+
const store = input.store ?? createIntegrationStateStore();
|
|
371
|
+
retryPendingPrunesOnce(store);
|
|
372
|
+
const io = input.io ?? store.io();
|
|
373
|
+
const spec = INTEGRATION_CLIENTS[input.clientId];
|
|
374
|
+
const exportSpec = EXPORT_CLIENTS[input.clientId];
|
|
375
|
+
const retention = retentionOf(input.clientId, store);
|
|
376
|
+
/*
|
|
377
|
+
* Resolution can refuse — a relative OPENCLAW_* selector names a file whose
|
|
378
|
+
* meaning depends on a working directory we cannot know. The LIST route asks
|
|
379
|
+
* every client for its state, so letting that escape would answer 500 for
|
|
380
|
+
* the whole Integrations page because one client is misconfigured.
|
|
381
|
+
*/
|
|
382
|
+
let configPath: string;
|
|
383
|
+
let installed: boolean;
|
|
384
|
+
try {
|
|
385
|
+
configPath = spec.configPath(input.env, input.home);
|
|
386
|
+
installed = io.statKind(spec.detectDir(input.env, input.home)) === "dir";
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
389
|
+
return {
|
|
390
|
+
clientId: input.clientId,
|
|
391
|
+
state: "unsafe",
|
|
392
|
+
installed: false,
|
|
393
|
+
configPath: "",
|
|
394
|
+
reason: "unresolvable-path",
|
|
395
|
+
...retention,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const target = loadTarget(io, configPath);
|
|
400
|
+
if (!target.ok) {
|
|
401
|
+
return {
|
|
402
|
+
clientId: input.clientId,
|
|
403
|
+
state: "unsafe",
|
|
404
|
+
installed,
|
|
405
|
+
configPath,
|
|
406
|
+
reason: target.why === "read-failed" ? "unparseable" : "not-regular-file",
|
|
407
|
+
...retention,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const parsed = parseConfig(target.before, exportSpec.format);
|
|
412
|
+
const contribution = exportSpec.buildContribution(exportContextOf(input));
|
|
413
|
+
const record = store.readRecords()[input.clientId] ?? null;
|
|
414
|
+
const { state, reason } = classifyIntegration({
|
|
415
|
+
fileText: target.before,
|
|
416
|
+
fileIsRegular: true,
|
|
417
|
+
parsed,
|
|
418
|
+
record,
|
|
419
|
+
contribution,
|
|
420
|
+
configPath,
|
|
421
|
+
clientId: input.clientId,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
clientId: input.clientId,
|
|
426
|
+
state,
|
|
427
|
+
installed,
|
|
428
|
+
configPath,
|
|
429
|
+
...(reason ? { reason } : {}),
|
|
430
|
+
...(record ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
|
|
431
|
+
...retention,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One store, bound once.
|
|
3
|
+
*
|
|
4
|
+
* Passing a directory per call was not enough: retention would honor it while
|
|
5
|
+
* records, the journal and snapshots still resolved the global root, so an
|
|
6
|
+
* operation that believed itself isolated could read and mutate the developer's
|
|
7
|
+
* real store. Every method here derives from one `root`, and `io()` binds the
|
|
8
|
+
* bookkeeping seams to that same store so the two can never point at different
|
|
9
|
+
* roots.
|
|
10
|
+
*
|
|
11
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/006 §Config-dir seam.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
appendOperation,
|
|
15
|
+
captureSnapshot,
|
|
16
|
+
clearPruneFailure,
|
|
17
|
+
countSnapshots,
|
|
18
|
+
findOperation,
|
|
19
|
+
listOperations,
|
|
20
|
+
markPruneFailure,
|
|
21
|
+
pruneSnapshots,
|
|
22
|
+
readMaintenance,
|
|
23
|
+
readSnapshot,
|
|
24
|
+
type JournalEntry,
|
|
25
|
+
type MaintenanceState,
|
|
26
|
+
type SnapshotRef,
|
|
27
|
+
} from "./journal";
|
|
28
|
+
import { fileIO, type IntegrationIO } from "./config-io";
|
|
29
|
+
import {
|
|
30
|
+
deleteRecord,
|
|
31
|
+
integrationsDir,
|
|
32
|
+
readRecords,
|
|
33
|
+
writeRecord,
|
|
34
|
+
type OwnershipRecord,
|
|
35
|
+
} from "./ownership";
|
|
36
|
+
import type { IntegrationClientId } from "./registry";
|
|
37
|
+
|
|
38
|
+
export interface IntegrationStateStore {
|
|
39
|
+
/** The integrations directory itself — never a config root to resolve again. */
|
|
40
|
+
readonly root: string;
|
|
41
|
+
readRecords(): Partial<Record<IntegrationClientId, OwnershipRecord>>;
|
|
42
|
+
putRecord(record: OwnershipRecord): void;
|
|
43
|
+
dropRecord(clientId: IntegrationClientId): void;
|
|
44
|
+
appendJournal(entry: JournalEntry): void;
|
|
45
|
+
listOperations(clientId?: IntegrationClientId, limit?: number): JournalEntry[];
|
|
46
|
+
findOperation(opId: string): JournalEntry | null;
|
|
47
|
+
captureSnapshot(clientId: IntegrationClientId, opId: string, text: string | null): SnapshotRef;
|
|
48
|
+
readSnapshot(entry: JournalEntry):
|
|
49
|
+
| { kind: "none" }
|
|
50
|
+
| { kind: "stored"; text: string; path: string }
|
|
51
|
+
| { kind: "expired" };
|
|
52
|
+
countSnapshots(clientId: IntegrationClientId): number | null;
|
|
53
|
+
pruneSnapshots(clientId: IntegrationClientId): { ok: true } | { ok: false; error: string };
|
|
54
|
+
readMaintenance(): MaintenanceState;
|
|
55
|
+
markPruneFailure(clientId: IntegrationClientId, error: string): void;
|
|
56
|
+
clearPruneFailure(clientId: IntegrationClientId): void;
|
|
57
|
+
retryPendingPrunes(): void;
|
|
58
|
+
/** Filesystem seam with bookkeeping bound to THIS store. */
|
|
59
|
+
io(): IntegrationIO;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Bind every integration state path to ONE root.
|
|
64
|
+
*
|
|
65
|
+
* Ownership-manifest note: writes go through `atomicWriteFile`, which records
|
|
66
|
+
* the target in the opencodex uninstall manifest. That registration is scoped
|
|
67
|
+
* by `manifestRelativePath` and refuses any path outside the process config
|
|
68
|
+
* dir, so a store rooted in a temp dir — the shape every test uses — touches
|
|
69
|
+
* no global state. A store rooted UNDER the real config dir does register its
|
|
70
|
+
* files there, which is correct: those files genuinely are opencodex-owned and
|
|
71
|
+
* should be removed on uninstall.
|
|
72
|
+
*/
|
|
73
|
+
export function createIntegrationStateStore(root: string = integrationsDir()): IntegrationStateStore {
|
|
74
|
+
const dir = root;
|
|
75
|
+
const store: IntegrationStateStore = {
|
|
76
|
+
root: dir,
|
|
77
|
+
readRecords: () => readRecords(dir),
|
|
78
|
+
putRecord: record => writeRecord(record, dir),
|
|
79
|
+
dropRecord: clientId => deleteRecord(clientId, dir),
|
|
80
|
+
appendJournal: entry => appendOperation(entry, dir),
|
|
81
|
+
listOperations: (clientId, limit) => listOperations(clientId, limit, dir),
|
|
82
|
+
findOperation: opId => findOperation(opId, dir),
|
|
83
|
+
captureSnapshot: (clientId, opId, text) => captureSnapshot(clientId, opId, text, dir),
|
|
84
|
+
readSnapshot: entry => readSnapshot(entry, dir),
|
|
85
|
+
countSnapshots: clientId => countSnapshots(clientId, dir),
|
|
86
|
+
pruneSnapshots: clientId => pruneSnapshots(clientId, dir),
|
|
87
|
+
readMaintenance: () => readMaintenance(dir),
|
|
88
|
+
markPruneFailure: (clientId, error) => markPruneFailure(clientId, error, dir),
|
|
89
|
+
clearPruneFailure: clientId => clearPruneFailure(clientId, dir),
|
|
90
|
+
retryPendingPrunes: () => {
|
|
91
|
+
for (const clientId of Object.keys(store.readMaintenance().pruneFailures) as IntegrationClientId[]) {
|
|
92
|
+
if (store.pruneSnapshots(clientId).ok) store.clearPruneFailure(clientId);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
io: () => ({
|
|
96
|
+
...fileIO(),
|
|
97
|
+
appendJournal: entry => store.appendJournal(entry),
|
|
98
|
+
putRecord: record => store.putRecord(record),
|
|
99
|
+
dropRecord: clientId => store.dropRecord(clientId),
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
return store;
|
|
103
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const LOCK_DEADLINE_MS = 2_000;
|
|
4
|
+
const LOCK_DELAYS_MS = [20, 40, 80, 160, 200] as const;
|
|
5
|
+
|
|
6
|
+
export interface IntegrationWriterLockSeams {
|
|
7
|
+
writeFile: (
|
|
8
|
+
path: string,
|
|
9
|
+
payload: string,
|
|
10
|
+
options: { flag: "wx"; mode: 0o600 },
|
|
11
|
+
) => Promise<void>;
|
|
12
|
+
removeFile: (path: string) => Promise<void>;
|
|
13
|
+
now: () => number;
|
|
14
|
+
delay: (milliseconds: number) => Promise<void>;
|
|
15
|
+
pid: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class IntegrationWriterLockBusyError extends Error {
|
|
19
|
+
constructor(readonly lockPath: string) {
|
|
20
|
+
super("integration_mutation_busy");
|
|
21
|
+
this.name = "IntegrationWriterLockBusyError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class IntegrationWriterLockIOError extends Error {
|
|
26
|
+
constructor(readonly lockPath: string, readonly operation: "acquire" | "release", cause: unknown) {
|
|
27
|
+
// The management route returns Error.message to its caller. Keep the
|
|
28
|
+
// private config path and OS diagnostic on typed fields/cause, not the wire.
|
|
29
|
+
super(`integration writer lock ${operation} failed`, { cause });
|
|
30
|
+
this.name = "IntegrationWriterLockIOError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultSeams: IntegrationWriterLockSeams = {
|
|
35
|
+
writeFile: async (path, payload, options) => { await writeFile(path, payload, options); },
|
|
36
|
+
// Match DSH rc.6: an already-absent lock is a successful release.
|
|
37
|
+
removeFile: async path => { await rm(path, { force: true }); },
|
|
38
|
+
now: () => Date.now(),
|
|
39
|
+
delay: milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)),
|
|
40
|
+
pid: process.pid,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function errorCode(error: unknown): string | undefined {
|
|
44
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
45
|
+
? String((error as { code?: unknown }).code)
|
|
46
|
+
: undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hold the exact sibling `<settings>.lock` around one complete transaction.
|
|
51
|
+
* A contender is never deleted: release runs only after our exclusive create
|
|
52
|
+
* succeeded.
|
|
53
|
+
*/
|
|
54
|
+
export async function withIntegrationWriterLock<T>(
|
|
55
|
+
configPath: string,
|
|
56
|
+
operation: () => Promise<T>,
|
|
57
|
+
seams: IntegrationWriterLockSeams = defaultSeams,
|
|
58
|
+
suffix: ".lock" = ".lock",
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
const lockPath = `${configPath}${suffix}`;
|
|
61
|
+
const startedAt = seams.now();
|
|
62
|
+
let delayIndex = 0;
|
|
63
|
+
for (;;) {
|
|
64
|
+
try {
|
|
65
|
+
await seams.writeFile(lockPath, `${seams.pid}\n`, { flag: "wx", mode: 0o600 });
|
|
66
|
+
break;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (errorCode(error) !== "EEXIST") {
|
|
69
|
+
throw new IntegrationWriterLockIOError(lockPath, "acquire", error);
|
|
70
|
+
}
|
|
71
|
+
const elapsedMs = seams.now() - startedAt;
|
|
72
|
+
if (elapsedMs >= LOCK_DEADLINE_MS) {
|
|
73
|
+
throw new IntegrationWriterLockBusyError(lockPath);
|
|
74
|
+
}
|
|
75
|
+
const backoffMs = LOCK_DELAYS_MS[Math.min(delayIndex, LOCK_DELAYS_MS.length - 1)]!;
|
|
76
|
+
// Keep the final retry inside the advertised two-second deadline.
|
|
77
|
+
const delayMs = Math.min(backoffMs, LOCK_DEADLINE_MS - elapsedMs);
|
|
78
|
+
delayIndex += 1;
|
|
79
|
+
await seams.delay(delayMs);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let outcome: { ok: true; value: T } | { ok: false; error: unknown };
|
|
84
|
+
try {
|
|
85
|
+
outcome = { ok: true, value: await operation() };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
outcome = { ok: false, error };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await seams.removeFile(lockPath);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Cleanup cannot replace the protected operation's actual failure.
|
|
93
|
+
if (!outcome.ok) throw outcome.error;
|
|
94
|
+
throw new IntegrationWriterLockIOError(lockPath, "release", error);
|
|
95
|
+
}
|
|
96
|
+
if (!outcome.ok) throw outcome.error;
|
|
97
|
+
return outcome.value;
|
|
98
|
+
}
|