@bitkyc08/opencodex 2.86.0 → 2.88.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/gui/dist/assets/App-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +77 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +46 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +33 -16
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +14 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +53 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-failure-model.ts +2 -0
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +40 -11
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +6 -5
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/opencode-zen-rate-limit.ts +7 -2
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +74 -6
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/analytics.ts +7 -15
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +117 -24
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/bridge/sse.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { anthropicProgressItems } from "../responses/anthropic-progress";
|
|
1
2
|
import type {
|
|
2
3
|
AdapterEvent,
|
|
4
|
+
AnthropicServerBlocks,
|
|
3
5
|
OcxMessagePhase,
|
|
4
6
|
OcxProviderContinuationState,
|
|
5
7
|
OcxProviderOpaqueToolCallMetadata,
|
|
@@ -12,7 +14,7 @@ import {
|
|
|
12
14
|
classifyError,
|
|
13
15
|
cyberPolicyErrorType,
|
|
14
16
|
CYBER_POLICY_ERROR_CODE,
|
|
15
|
-
|
|
17
|
+
isTerminalRefusalCode,
|
|
16
18
|
type OcxErrorPayload,
|
|
17
19
|
} from "../lib/errors";
|
|
18
20
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
} from "../lib/translator-budget";
|
|
51
53
|
import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
|
|
52
54
|
import type { OutputItem, StringChunks } from "./internal";
|
|
55
|
+
import { terminalRefusalFailure } from "./terminal-refusal";
|
|
53
56
|
|
|
54
57
|
function sseEvent(name: string, data: Record<string, unknown>): string {
|
|
55
58
|
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
@@ -88,6 +91,7 @@ export function bridgeToResponsesSSE(
|
|
|
88
91
|
* response.completed — codex-rs collect_compaction_output requires exactly one.
|
|
89
92
|
*/
|
|
90
93
|
compaction?: boolean;
|
|
94
|
+
compactionRetention?: { encode(summary: string): string; commit(): void };
|
|
91
95
|
/** One-shot: first non-empty text/thinking/raw-reasoning/tool-input delta observed. */
|
|
92
96
|
onFirstOutput?: () => void;
|
|
93
97
|
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
@@ -120,6 +124,12 @@ export function bridgeToResponsesSSE(
|
|
|
120
124
|
* would reach the client under the invented name instead of the normalized one.
|
|
121
125
|
*/
|
|
122
126
|
enforceDeclaredToolNames?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Deliver a non-retryable upstream refusal as `response.failed` with a terminal-refusal code
|
|
129
|
+
* instead of `response.incomplete` (see `./terminal-refusal`). Set only for the Responses
|
|
130
|
+
* inbound wire: the chat and Anthropic encoders read the incomplete to report a filter stop.
|
|
131
|
+
*/
|
|
132
|
+
terminalRefusalAsFailed?: boolean;
|
|
123
133
|
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
124
134
|
toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
|
|
125
135
|
/**
|
|
@@ -676,12 +686,15 @@ export function bridgeToResponsesSSE(
|
|
|
676
686
|
// leaves a "Searching the web" spinner spinning forever.
|
|
677
687
|
// `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
|
|
678
688
|
// downstream translators (claude outbound) can fill web_search_tool_result content.
|
|
679
|
-
|
|
689
|
+
// `anthropic` (Anthropic's server-side call) rides verbatim as `anthropic_blocks`, for the same
|
|
690
|
+
// claude outbound translator; a web_fetch makes the cell a page open.
|
|
691
|
+
const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], anthropic?: AnthropicServerBlocks) => {
|
|
680
692
|
if (!currentWebSearch) return;
|
|
681
693
|
const item = {
|
|
682
694
|
type: "web_search_call", id: currentWebSearch.itemId, status,
|
|
683
|
-
action: webSearchAction(queries),
|
|
695
|
+
action: webSearchAction(queries, anthropic),
|
|
684
696
|
...(sources && sources.length > 0 ? { sources } : {}),
|
|
697
|
+
...(anthropic ? { anthropic_blocks: anthropic } : {}),
|
|
685
698
|
};
|
|
686
699
|
emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
|
|
687
700
|
retainFinishedItem(item as OutputItem);
|
|
@@ -841,6 +854,7 @@ export function bridgeToResponsesSSE(
|
|
|
841
854
|
// its compaction UI renders nothing mid-turn, so nothing is lost visually.
|
|
842
855
|
if (options?.compaction) {
|
|
843
856
|
if (event.type === "text_delta") {
|
|
857
|
+
if (event.anthropicThinkingSignature) continue;
|
|
844
858
|
compaction = appendString(
|
|
845
859
|
compaction,
|
|
846
860
|
event.text,
|
|
@@ -871,6 +885,29 @@ export function bridgeToResponsesSSE(
|
|
|
871
885
|
break;
|
|
872
886
|
}
|
|
873
887
|
case "text_delta": {
|
|
888
|
+
if (event.anthropicThinkingSignature) {
|
|
889
|
+
rawReasoningForNextToolCall = "";
|
|
890
|
+
closeCurrentMessage("commentary");
|
|
891
|
+
closeCurrentReasoning();
|
|
892
|
+
closeCurrentRawReasoning();
|
|
893
|
+
flushHiddenRawReasoning();
|
|
894
|
+
flushHiddenReasoningEnvelope();
|
|
895
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
896
|
+
for (const item of anthropicProgressItems(event.text, event.anthropicThinkingSignature, budget)) {
|
|
897
|
+
emit("response.output_item.added", { output_index: outputIndex, item: { ...item, ...(item.type === "message" ? { status: "in_progress", content: [] } : {}) } });
|
|
898
|
+
if (item.type === "message") {
|
|
899
|
+
const part = item.content![0];
|
|
900
|
+
emit("response.content_part.added", { item_id: item.id, output_index: outputIndex, content_index: 0, part: { ...part, text: "" } });
|
|
901
|
+
emit("response.output_text.delta", { item_id: item.id, output_index: outputIndex, content_index: 0, delta: event.text });
|
|
902
|
+
emit("response.output_text.done", { item_id: item.id, output_index: outputIndex, content_index: 0, text: event.text });
|
|
903
|
+
emit("response.content_part.done", { item_id: item.id, output_index: outputIndex, content_index: 0, part });
|
|
904
|
+
}
|
|
905
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
906
|
+
retainFinishedItem(item as OutputItem);
|
|
907
|
+
outputIndex++;
|
|
908
|
+
}
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
874
911
|
if (currentReasoning) closeCurrentReasoning();
|
|
875
912
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
876
913
|
flushHiddenRawReasoning();
|
|
@@ -1195,7 +1232,7 @@ export function bridgeToResponsesSSE(
|
|
|
1195
1232
|
currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
|
|
1196
1233
|
}
|
|
1197
1234
|
const safeSources = safeWebSearchSources(event.sources);
|
|
1198
|
-
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources);
|
|
1235
|
+
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.anthropic);
|
|
1199
1236
|
// Queue this search's sources for the next assistant message (dedup by URL).
|
|
1200
1237
|
if (safeSources.length > 0) {
|
|
1201
1238
|
for (const source of safeSources) {
|
|
@@ -1233,7 +1270,7 @@ export function bridgeToResponsesSSE(
|
|
|
1233
1270
|
// Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
|
|
1234
1271
|
const item = {
|
|
1235
1272
|
type: "compaction", id: `cmp_${uuid()}`,
|
|
1236
|
-
encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(compaction)),
|
|
1273
|
+
encrypted_content: event.compactionEncryptedContent ?? (options?.compactionRetention?.encode(joinChunks(compaction)) ?? encodeCompactionSummary(joinChunks(compaction))),
|
|
1237
1274
|
};
|
|
1238
1275
|
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
1239
1276
|
retainFinishedItem(item as OutputItem, event.compactionEncryptedContent ? 0 : compaction.bytes);
|
|
@@ -1271,6 +1308,7 @@ export function bridgeToResponsesSSE(
|
|
|
1271
1308
|
emit("response.completed", {
|
|
1272
1309
|
response,
|
|
1273
1310
|
});
|
|
1311
|
+
if (!closed && !terminated && options?.compaction && !event.compactionEncryptedContent) options.compactionRetention?.commit();
|
|
1274
1312
|
reportTerminal("completed");
|
|
1275
1313
|
}
|
|
1276
1314
|
terminalEvent = true;
|
|
@@ -1287,12 +1325,25 @@ export function bridgeToResponsesSSE(
|
|
|
1287
1325
|
flushHiddenReasoningEnvelope();
|
|
1288
1326
|
options?.onUsage?.(event.usage);
|
|
1289
1327
|
await awaitThoughtSignatureDurability();
|
|
1328
|
+
const refusal = options?.terminalRefusalAsFailed ? terminalRefusalFailure(event) : undefined;
|
|
1329
|
+
if (refusal) {
|
|
1330
|
+
emit("response.failed", {
|
|
1331
|
+
response: {
|
|
1332
|
+
...responseSnapshot("failed", finishedItems, event.endTurn),
|
|
1333
|
+
usage: responsesUsage(event.usage),
|
|
1334
|
+
...refusal,
|
|
1335
|
+
},
|
|
1336
|
+
});
|
|
1337
|
+
reportTerminal("failed");
|
|
1338
|
+
terminalEvent = true;
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1290
1341
|
emit("response.incomplete", {
|
|
1291
1342
|
response: {
|
|
1292
1343
|
...responseSnapshot("incomplete", finishedItems, event.endTurn),
|
|
1293
1344
|
usage: responsesUsage(event.usage),
|
|
1294
1345
|
incomplete_details: {
|
|
1295
|
-
reason: event.reason,
|
|
1346
|
+
reason: truncationReasonFor(event.reason) ?? event.reason,
|
|
1296
1347
|
...(event.message ? { message: event.message } : {}),
|
|
1297
1348
|
...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
1298
1349
|
},
|
|
@@ -1325,7 +1376,7 @@ export function bridgeToResponsesSSE(
|
|
|
1325
1376
|
...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
|
|
1326
1377
|
error: failure.error,
|
|
1327
1378
|
last_error: failure.error,
|
|
1328
|
-
...(
|
|
1379
|
+
...(isTerminalRefusalCode(failure.error.code)
|
|
1329
1380
|
? { retryable: false }
|
|
1330
1381
|
: event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
1331
1382
|
},
|
|
@@ -1366,7 +1417,7 @@ export function bridgeToResponsesSSE(
|
|
|
1366
1417
|
...responseSnapshot("failed", finishedItems),
|
|
1367
1418
|
error: failure,
|
|
1368
1419
|
last_error: failure,
|
|
1369
|
-
...(
|
|
1420
|
+
...(isTerminalRefusalCode(failure.code) ? { retryable: false } : {}),
|
|
1370
1421
|
},
|
|
1371
1422
|
});
|
|
1372
1423
|
reportTerminal("failed");
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AdapterEvent } from "../types";
|
|
2
|
+
import type { OcxErrorPayload } from "../lib/errors";
|
|
3
|
+
import { redactSecretString } from "../lib/redact";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The terminal-refusal code Codex ends a turn on. It is a member of `TERMINAL_REFUSAL_CODES`
|
|
7
|
+
* in `src/lib/errors.ts`, and the one that says "this prompt, as sent, is refused" without
|
|
8
|
+
* claiming a specific policy family the upstream never named.
|
|
9
|
+
*/
|
|
10
|
+
const TERMINAL_REFUSAL_FAILED_CODE = "invalid_prompt";
|
|
11
|
+
|
|
12
|
+
interface TerminalRefusalFailure {
|
|
13
|
+
error: OcxErrorPayload;
|
|
14
|
+
last_error: OcxErrorPayload;
|
|
15
|
+
retryable: false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Client-visible copy: what happened, and that resending the same context cannot help. It points
|
|
20
|
+
* at a new conversation rather than a new turn, because a later turn replays the refused context.
|
|
21
|
+
*/
|
|
22
|
+
function terminalRefusalMessage(detail: string | undefined): string {
|
|
23
|
+
const reason = detail?.trim() ? ` (${redactSecretString(detail.trim())})` : "";
|
|
24
|
+
return `The upstream model refused this request${reason}. `
|
|
25
|
+
+ "Retrying with the same conversation context will not succeed; change the request or start a new conversation.";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An adapter `incomplete` that is a non-retryable upstream refusal: a content filter carrying an
|
|
30
|
+
* explicit `retryable: false` verdict. A filter without the verdict, an output cap, a stall and an
|
|
31
|
+
* adapter EOF do not qualify. The sidecar loops read this too: a refused iteration ends the turn,
|
|
32
|
+
* so a sidecar call it carried is neither run nor answered with another model request.
|
|
33
|
+
*/
|
|
34
|
+
export function isTerminalRefusalIncomplete(event: AdapterEvent | undefined): boolean {
|
|
35
|
+
return event?.type === "incomplete" && event.reason === "content_filter" && event.retryable === false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `response.failed` fields for an adapter `incomplete` that is a non-retryable upstream refusal
|
|
40
|
+
* (#4312), or `undefined` when the event is anything else. The Anthropic and Kiro adapters are
|
|
41
|
+
* the ones that emit it today.
|
|
42
|
+
*
|
|
43
|
+
* codex-rs maps every `response.incomplete` to a retryable stream error and never reads
|
|
44
|
+
* `incomplete_details.retryable`, so a refusal delivered that way is re-sent through the whole
|
|
45
|
+
* stream retry budget. It only ends the turn on `response.failed` carrying a terminal-refusal
|
|
46
|
+
* `error.code`. Only an explicit `retryable: false` content filter qualifies: a filter without
|
|
47
|
+
* the verdict, an output cap, a stall and an adapter EOF stay `response.incomplete`.
|
|
48
|
+
*/
|
|
49
|
+
export function terminalRefusalFailure(
|
|
50
|
+
event: Extract<AdapterEvent, { type: "incomplete" }>,
|
|
51
|
+
): TerminalRefusalFailure | undefined {
|
|
52
|
+
if (!isTerminalRefusalIncomplete(event)) return undefined;
|
|
53
|
+
const error: OcxErrorPayload = {
|
|
54
|
+
type: "invalid_request_error",
|
|
55
|
+
code: TERMINAL_REFUSAL_FAILED_CODE,
|
|
56
|
+
message: terminalRefusalMessage(event.message),
|
|
57
|
+
};
|
|
58
|
+
return { error, last_error: error, retryable: false };
|
|
59
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, constants,
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, type BigIntStats } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
|
|
@@ -84,9 +84,20 @@ export function readDesktopLibraryText(path: string): string {
|
|
|
84
84
|
return readDesktopLibraryBytes(path).toString("utf8");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Only an absent directory entry is optional; dangling links and failed reads still refuse. */
|
|
88
|
+
export function readOptionalDesktopLibraryBytes(path: string): Buffer | undefined {
|
|
89
|
+
try { return readDesktopLibraryBytes(path); }
|
|
90
|
+
catch (error) {
|
|
91
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Read optional verified metadata; a present file must contain an entries array. */
|
|
87
97
|
export function parseMetadata(path: string): Desktop3pMetadata {
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
const bytes = readOptionalDesktopLibraryBytes(path);
|
|
99
|
+
if (bytes === undefined) return { entries: [] };
|
|
100
|
+
const parsed = JSON.parse(bytes.toString("utf8")) as Partial<Desktop3pMetadata>;
|
|
90
101
|
if (!Array.isArray(parsed.entries)) throw new Error("Claude Desktop 3P _meta.json has no entries array");
|
|
91
102
|
return { ...parsed, entries: parsed.entries };
|
|
92
103
|
}
|
|
@@ -120,11 +131,25 @@ export const OPENCODEX_DESKTOP_PROFILE_KEYS = new Set([
|
|
|
120
131
|
"inferenceModels",
|
|
121
132
|
]);
|
|
122
133
|
|
|
123
|
-
export
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
export interface DesktopProfileSnapshot {
|
|
135
|
+
/** Verified bytes of the existing profile; undefined only when the entry is absent. */
|
|
136
|
+
bytes?: Buffer;
|
|
137
|
+
foreign: Record<string, unknown>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Capture verified bytes for key preservation, backup and rollback; publication revalidates them. */
|
|
141
|
+
export function readDesktopProfileSnapshot(path: string): DesktopProfileSnapshot {
|
|
142
|
+
const bytes = readOptionalDesktopLibraryBytes(path);
|
|
143
|
+
if (bytes === undefined) return { foreign: {} };
|
|
144
|
+
const parsed = JSON.parse(bytes.toString("utf8")) as unknown;
|
|
126
145
|
if (!isRecord(parsed)) throw new Error("Claude Desktop 3P profile is not a JSON object");
|
|
127
|
-
|
|
146
|
+
const foreign = Object.fromEntries(
|
|
128
147
|
Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
|
|
129
148
|
);
|
|
149
|
+
return { bytes, foreign };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Preserve settings outside the gateway-owned keys from a verified profile. */
|
|
153
|
+
export function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
|
|
154
|
+
return readDesktopProfileSnapshot(path).foreign;
|
|
130
155
|
}
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -5,15 +5,16 @@ import { applyRemoteDesktopStore, inspectRemoteDesktopCleanup, restoreRemoteDesk
|
|
|
5
5
|
import { canonicalDirectory } from "./desktop-remote-store-io";
|
|
6
6
|
import {
|
|
7
7
|
resolveDesktop3pConfigLibraryPath, parseMetadata, SAFE_DESKTOP_PROFILE_ID, isRecord,
|
|
8
|
-
isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath,
|
|
8
|
+
isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileSnapshot, readDesktopLibraryText, readDesktopLibraryBytes,
|
|
9
|
+
readOptionalDesktopLibraryBytes,
|
|
9
10
|
type Desktop3pConfigLibraryOptions, type Desktop3pMetadata, type Desktop3pMetadataEntry,
|
|
10
11
|
} from "./desktop-3p-library";
|
|
11
12
|
export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
|
|
12
13
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
-
import {
|
|
14
|
+
import { existsSync, mkdirSync, lstatSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { join } from "node:path";
|
|
15
16
|
import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
|
|
16
|
-
import { atomicWriteFileNoFollow } from "../config/atomic-write";
|
|
17
|
+
import { atomicWriteFileNoFollow, atomicWriteFileStreamed } from "../config/atomic-write";
|
|
17
18
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
18
19
|
import type { OcxClaudeDesktopProfile } from "../types";
|
|
19
20
|
import {
|
|
@@ -800,6 +801,7 @@ function desktopRefreshPathsSafe(libraryPath: string, configPath?: string): bool
|
|
|
800
801
|
} catch { return false; }
|
|
801
802
|
}
|
|
802
803
|
|
|
804
|
+
/** Publish a gateway profile from a verified snapshot, rolling it back if metadata fails. */
|
|
803
805
|
function writeDesktop3pConfigWithGenerator(
|
|
804
806
|
generate: () => object,
|
|
805
807
|
refreshOnly?: { requiredId: string; expectedFingerprint: string; admit: () => boolean },
|
|
@@ -828,8 +830,8 @@ function writeDesktop3pConfigWithGenerator(
|
|
|
828
830
|
: [...metadata.entries, entry];
|
|
829
831
|
|
|
830
832
|
const generated = generate();
|
|
831
|
-
const
|
|
832
|
-
const configJson = JSON.stringify({ ...
|
|
833
|
+
const previous = readDesktopProfileSnapshot(configPath);
|
|
834
|
+
const configJson = JSON.stringify({ ...previous.foreign, ...generated }, null, 2) + "\n";
|
|
833
835
|
const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
|
|
834
836
|
if (refreshOnly) {
|
|
835
837
|
atomicWriteFileNoFollow(configPath, configJson, undefined, {
|
|
@@ -847,12 +849,31 @@ function writeDesktop3pConfigWithGenerator(
|
|
|
847
849
|
});
|
|
848
850
|
return { written: true, path: configPath, fingerprint };
|
|
849
851
|
}
|
|
850
|
-
|
|
852
|
+
atomicReplaceDesktopConfig(configPath, configJson, previous.bytes);
|
|
851
853
|
try {
|
|
852
854
|
atomicWriteFile(metadataPath, JSON.stringify({ ...metadata, appliedId: id, entries }, null, 2) + "\n");
|
|
853
855
|
} catch (metaError) {
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
+
// Roll back from the verified bytes in memory: reopening the backup could meet a swapped
|
|
857
|
+
// entry and leave the new profile beside the old metadata. A profile this call created is
|
|
858
|
+
// unreferenced once metadata failed, so remove it rather than orphan one per retry.
|
|
859
|
+
// Rollback is ownership-aware: it acts only while the live profile still holds the bytes
|
|
860
|
+
// this call published, checked by the same comparison that guarded publication.
|
|
861
|
+
const base = metaError instanceof Error ? metaError.message : String(metaError);
|
|
862
|
+
const published = Buffer.from(configJson);
|
|
863
|
+
let owned = false;
|
|
864
|
+
const stillOwned = () => { owned = false; assertDesktopProfileUnchanged(configPath, published); owned = true; };
|
|
865
|
+
try {
|
|
866
|
+
stillOwned();
|
|
867
|
+
if (previous.bytes !== undefined) writeDesktopConfigBytes(configPath, previous.bytes, stillOwned);
|
|
868
|
+
else unlinkSync(configPath);
|
|
869
|
+
} catch (rollbackError) {
|
|
870
|
+
if (!owned) {
|
|
871
|
+
// Another writer replaced the profile after publication; its bytes win over our rollback.
|
|
872
|
+
throw new Error(`${base} (profile rollback skipped: profile changed after publish)`, { cause: metaError });
|
|
873
|
+
}
|
|
874
|
+
const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
875
|
+
throw new AggregateError([metaError, rollbackError], `${base} (profile rollback also failed: ${detail})`, { cause: metaError });
|
|
876
|
+
}
|
|
856
877
|
throw metaError;
|
|
857
878
|
}
|
|
858
879
|
return { written: true, path: configPath, fingerprint };
|
|
@@ -862,14 +883,35 @@ function writeDesktop3pConfigWithGenerator(
|
|
|
862
883
|
}
|
|
863
884
|
}
|
|
864
885
|
|
|
865
|
-
/**
|
|
886
|
+
/**
|
|
887
|
+
* Compare the live profile with the caller's verified bytes before backup and before replacement.
|
|
888
|
+
* Refuse drift without retrying; backup and rollback continue to use the original verified bytes.
|
|
889
|
+
* `previous` is undefined only for an absent profile, which leaves any older backup alone.
|
|
890
|
+
* Exported for failure-path tests.
|
|
891
|
+
*/
|
|
866
892
|
export function atomicReplaceDesktopConfig(
|
|
867
893
|
path: string,
|
|
868
894
|
content: string,
|
|
869
|
-
|
|
895
|
+
previous: Buffer | undefined,
|
|
896
|
+
writer: typeof atomicWriteFileNoFollow = atomicWriteFileNoFollow,
|
|
870
897
|
): { backupPath?: string } {
|
|
898
|
+
const validateBeforeRename = () => assertDesktopProfileUnchanged(path, previous);
|
|
899
|
+
validateBeforeRename();
|
|
871
900
|
const backupPath = `${path}.bak`;
|
|
872
|
-
if (
|
|
873
|
-
writer(path, content);
|
|
874
|
-
return
|
|
901
|
+
if (previous !== undefined) writeDesktopConfigBytes(backupPath, previous);
|
|
902
|
+
writer(path, content, undefined, { validateBeforeRename });
|
|
903
|
+
return previous === undefined ? {} : { backupPath };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** Single compare-and-swap check: the live profile must still hold `expected` (undefined = absent). */
|
|
907
|
+
function assertDesktopProfileUnchanged(path: string, expected: Buffer | undefined): void {
|
|
908
|
+
const current = readOptionalDesktopLibraryBytes(path);
|
|
909
|
+
if (expected === undefined ? current !== undefined : current === undefined || !expected.equals(current)) {
|
|
910
|
+
throw new Error("desktop_profile_changed");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Publish the verified bytes without reopening the source or decoding its backup contents. */
|
|
915
|
+
function writeDesktopConfigBytes(path: string, bytes: Buffer, validateBeforeRename?: () => void): void {
|
|
916
|
+
atomicWriteFileStreamed(path, descriptor => { writeFileSync(descriptor, bytes); }, validateBeforeRename ? { validateBeforeRename } : {});
|
|
875
917
|
}
|
|
@@ -25,7 +25,7 @@ import type { OcxConfig } from "../types";
|
|
|
25
25
|
import { claudeConfigDir } from "./auth-detect";
|
|
26
26
|
import { inspectDesktop3pConfigLibrary } from "./desktop-3p";
|
|
27
27
|
import { claudeInterceptCaCertPath, ensureLocalInterceptCa } from "./intercept/local-ca";
|
|
28
|
-
import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/
|
|
28
|
+
import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/port-policy";
|
|
29
29
|
import { ensureClaudeInterceptProxyToken, readClaudeInterceptProxyToken } from "./intercept/proxy-auth";
|
|
30
30
|
import {
|
|
31
31
|
applyClaudeInterceptSettings,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type DesktopPickerProfileOptions,
|
|
24
24
|
} from "./desktop-picker-profile";
|
|
25
25
|
import { resolveClaudeDesktopMode, observeClaudeDesktopMode } from "./desktop-first-party";
|
|
26
|
+
import { supportsDesktopPicker } from "./desktop-picker-platform";
|
|
26
27
|
|
|
27
28
|
/** Desktop no longer selects the picker profile, so removing the CA's trust cannot strand it. */
|
|
28
29
|
function profileReleased(profile: DesktopPickerProfileInspection): boolean {
|
|
@@ -77,6 +78,8 @@ export interface DesktopPickerControllerDeps {
|
|
|
77
78
|
persistPreference: (value: boolean) => boolean;
|
|
78
79
|
/** Bound picker CONNECT proxy port (Desktop's egress), or null when it is not running. */
|
|
79
80
|
proxyPort: () => number | null;
|
|
81
|
+
/** Bind a dormant relay on explicit enable, under the controller's mutation lock. */
|
|
82
|
+
ensureProxy?: () => Promise<void>;
|
|
80
83
|
configDir: string;
|
|
81
84
|
persistentAuthority?: { store?: PickerCaStore };
|
|
82
85
|
security?: SecurityRunner;
|
|
@@ -91,7 +94,7 @@ function profileOptions(deps: DesktopPickerControllerDeps): DesktopPickerProfile
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
function statusReasonForConfig(config: OcxConfig, platform: NodeJS.Platform, proxyBound: boolean): DesktopPickerReason {
|
|
94
|
-
if (platform
|
|
97
|
+
if (!supportsDesktopPicker(platform)) return "unsupported_platform";
|
|
95
98
|
if (resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party") return "not_first_party";
|
|
96
99
|
if (!claudeDesktopIntegrationEnabled(config)) return "integration_off";
|
|
97
100
|
if (config.claudeCode?.intercept?.picker === false) return "disabled";
|
|
@@ -101,11 +104,11 @@ function statusReasonForConfig(config: OcxConfig, platform: NodeJS.Platform, pro
|
|
|
101
104
|
|
|
102
105
|
function emptyStatus(config: OcxConfig, platform: NodeJS.Platform, reason: DesktopPickerReason): DesktopPickerStatus {
|
|
103
106
|
return {
|
|
104
|
-
desired: platform
|
|
107
|
+
desired: supportsDesktopPicker(platform)
|
|
105
108
|
&& claudeDesktopIntegrationEnabled(config)
|
|
106
109
|
&& resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) === "first-party"
|
|
107
110
|
&& config.claudeCode?.intercept?.picker !== false,
|
|
108
|
-
supported: platform
|
|
111
|
+
supported: supportsDesktopPicker(platform),
|
|
109
112
|
trust: "unknown",
|
|
110
113
|
profile: "absent",
|
|
111
114
|
listenerReady: false,
|
|
@@ -142,7 +145,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
|
|
|
142
145
|
const trust = trustOverride ?? runtime.trust;
|
|
143
146
|
const effective = runtime.effective && profileCurrent && trust === "trusted";
|
|
144
147
|
let reason: DesktopPickerReason;
|
|
145
|
-
if (platform
|
|
148
|
+
if (!supportsDesktopPicker(platform)) reason = "unsupported_platform";
|
|
146
149
|
else if (!runtime.desired) reason = statusReasonForConfig(deps.readConfig(), platform, deps.proxyPort() !== null);
|
|
147
150
|
else if (deps.proxyPort() === null) reason = "proxy_unavailable";
|
|
148
151
|
else if (trust !== "trusted") reason = "trust_pending";
|
|
@@ -178,7 +181,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
|
|
|
178
181
|
|
|
179
182
|
async function untrustCurrentCa(): Promise<boolean> {
|
|
180
183
|
const caPath = pickerCaCertPath(deps.configDir);
|
|
181
|
-
if (platform
|
|
184
|
+
if (!supportsDesktopPicker(platform) || !existsSync(caPath)) return true;
|
|
182
185
|
try {
|
|
183
186
|
const sha1 = pickerCaFingerprints(readFileSync(caPath, "utf8")).sha1;
|
|
184
187
|
return (await untrustPickerCa(caPath, sha1, deps.security, platform)).ok;
|
|
@@ -207,11 +210,11 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
|
|
|
207
210
|
return withTrustFailure({ ...runtimeStatus(observedTrust), reason }, trustFailed);
|
|
208
211
|
};
|
|
209
212
|
const check = (config: OcxConfig, includePreference: boolean): DesktopPickerReason | null => {
|
|
210
|
-
if (platform
|
|
213
|
+
if (!supportsDesktopPicker(platform)) return "unsupported_platform";
|
|
211
214
|
if (resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party") return "mode_not_committed";
|
|
212
215
|
if (!claudeDesktopIntegrationEnabled(config)) return "integration_off";
|
|
213
216
|
if (includePreference && config.claudeCode?.intercept?.picker === false) return "disabled";
|
|
214
|
-
if (deps.proxyPort() === null) return "proxy_unavailable";
|
|
217
|
+
if (deps.proxyPort() === null && !deps.ensureProxy) return "proxy_unavailable";
|
|
215
218
|
return null;
|
|
216
219
|
};
|
|
217
220
|
|
|
@@ -224,6 +227,10 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
|
|
|
224
227
|
}
|
|
225
228
|
const second = check(deps.readConfig(), true);
|
|
226
229
|
if (second) return refuse(second);
|
|
230
|
+
if (deps.proxyPort() === null) {
|
|
231
|
+
try { await deps.ensureProxy?.(); } catch { return refuse("proxy_unavailable"); }
|
|
232
|
+
}
|
|
233
|
+
if (deps.proxyPort() === null) return refuse("proxy_unavailable");
|
|
227
234
|
|
|
228
235
|
let caPath: string;
|
|
229
236
|
let caSha1: string;
|
|
@@ -336,7 +343,7 @@ export async function removeDesktopPickerArtifacts(options: { configDir?: string
|
|
|
336
343
|
return { ok: false, residual };
|
|
337
344
|
}
|
|
338
345
|
const caPath = pickerCaCertPath(configDir);
|
|
339
|
-
if (platform
|
|
346
|
+
if (supportsDesktopPicker(platform) && existsSync(caPath)) {
|
|
340
347
|
try {
|
|
341
348
|
const sha1 = pickerCaFingerprints(readFileSync(caPath, "utf8")).sha1;
|
|
342
349
|
if (!(await untrustPickerCa(caPath, sha1, options.security, platform)).ok) residual.push("trust");
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { connect } from "node:net";
|
|
3
|
+
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
2
4
|
import {
|
|
3
5
|
readWindowsProxyBypassRegistry,
|
|
4
6
|
readWindowsSystemProxy,
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
} from "../lib/windows-system-proxy";
|
|
9
11
|
import { inspectDesktopFirstParty, observeClaudeDesktopMode } from "./desktop-first-party";
|
|
10
12
|
import { desktopFirstPartyDesired } from "./first-party-settings";
|
|
13
|
+
import { inspectDesktopPickerProfile, type DesktopPickerProfileInspection } from "./desktop-picker-profile";
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Whether the Windows system proxy silently bypasses Desktop first-party mode.
|
|
@@ -17,7 +20,8 @@ import { desktopFirstPartyDesired } from "./first-party-settings";
|
|
|
17
20
|
* as `HTTPS_PROXY`/`HTTP_PROXY`. That inherited value takes precedence over the `env` block
|
|
18
21
|
* OpenCodex writes into the user's `~/.claude/settings.json` (only Claude Code managed settings
|
|
19
22
|
* override it), so Code-tab traffic goes to the system proxy and never reaches the intercept.
|
|
20
|
-
*
|
|
23
|
+
* A selected picker profile with an available fixed egress relay takes precedence instead.
|
|
24
|
+
* Otherwise routed subagent models fail. A system proxy such as Clash or
|
|
21
25
|
* v2rayN, with no bypass for the API host, is the common case.
|
|
22
26
|
*
|
|
23
27
|
* Observe-only: this reads the registry and never changes it. A PAC script and WPAD automatic
|
|
@@ -123,6 +127,39 @@ export interface DesktopSystemProxyDeps {
|
|
|
123
127
|
firstPartyState?: () => DesktopFirstPartySettingsState;
|
|
124
128
|
readSystemProxy?: () => WindowsSystemProxyResult;
|
|
125
129
|
readBypass?: () => WindowsProxyBypassValues | null;
|
|
130
|
+
inspectPickerProfile?: () => DesktopPickerProfileInspection;
|
|
131
|
+
pickerRelayAvailable?: (proxyUrl: string) => boolean | Promise<boolean>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Observe the selected endpoint even in a separate doctor process or a blind-only recovery relay. */
|
|
135
|
+
async function livePickerRelayAvailable(proxyUrl: string): Promise<boolean> {
|
|
136
|
+
const port = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/.exec(proxyUrl)?.[1];
|
|
137
|
+
if (!port || Number(port) > 65535) return false;
|
|
138
|
+
try {
|
|
139
|
+
// A listening TCP socket alone is not evidence of an OpenCodex CONNECT relay.
|
|
140
|
+
const response = await directLocalHttpFetch(proxyUrl, { signal: AbortSignal.timeout(1_000) });
|
|
141
|
+
if (response.status !== 405 || response.headers.get("allow") !== "CONNECT"
|
|
142
|
+
|| !(await response.text()).startsWith("OpenCodex Claude intercept proxy accepts only CONNECT tunnels.")) return false;
|
|
143
|
+
// The main intercept proxy has the same HTTP response but requires credentials the Desktop
|
|
144
|
+
// profile cannot send. A loopback CONNECT is denied before dialing or choosing any tunnel;
|
|
145
|
+
// 403 proves unauthenticated admission, whereas the credential-bearing proxy returns 407.
|
|
146
|
+
return await new Promise<boolean>(resolve => {
|
|
147
|
+
const socket = connect({ host: "127.0.0.1", port: Number(port) });
|
|
148
|
+
const finish = (available: boolean) => { clearTimeout(timer); socket.destroy(); resolve(available); };
|
|
149
|
+
const timer = setTimeout(() => finish(false), 1_000);
|
|
150
|
+
let head = "";
|
|
151
|
+
socket.once("error", () => finish(false));
|
|
152
|
+
socket.once("close", () => finish(false));
|
|
153
|
+
socket.once("connect", () => socket.write("CONNECT 127.0.0.1:1 HTTP/1.1\r\nHost: 127.0.0.1:1\r\n\r\n"));
|
|
154
|
+
socket.on("data", bytes => {
|
|
155
|
+
head += bytes.toString("latin1");
|
|
156
|
+
if (head.includes("\r\n\r\n")) finish(/^HTTP\/1\.[01] 403 Forbidden\r\n/.test(head));
|
|
157
|
+
else if (head.length > 8 * 1024) finish(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
} catch { // no-excuse-ok: catch -- failed relay probes retain the registry diagnostic.
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
126
163
|
}
|
|
127
164
|
|
|
128
165
|
function liveFirstPartyState(
|
|
@@ -137,15 +174,24 @@ function liveFirstPartyState(
|
|
|
137
174
|
}
|
|
138
175
|
}
|
|
139
176
|
|
|
140
|
-
/**
|
|
141
|
-
export function collectDesktopSystemProxy(
|
|
177
|
+
/** Registry reads run only on Windows with a Desktop first-party env and no verified picker relay. */
|
|
178
|
+
export async function collectDesktopSystemProxy(
|
|
142
179
|
config: Pick<OcxConfig, "claudeCode" | "clientIntegrations" | "port" | "runtimeRole">,
|
|
143
180
|
deps: DesktopSystemProxyDeps = {},
|
|
144
|
-
): DesktopSystemProxyAssessment {
|
|
181
|
+
): Promise<DesktopSystemProxyAssessment> {
|
|
145
182
|
const platform = deps.platform ?? process.platform;
|
|
146
183
|
if (platform !== "win32") return { kind: "not-applicable" };
|
|
147
184
|
const firstParty = (deps.firstPartyState ?? (() => liveFirstPartyState(config)))();
|
|
148
185
|
if (firstParty === "off") return { kind: "not-applicable" };
|
|
186
|
+
if (firstParty === "applied" && config.claudeCode?.intercept?.picker !== false) {
|
|
187
|
+
try {
|
|
188
|
+
const profile = (deps.inspectPickerProfile ?? (() => inspectDesktopPickerProfile({ platform })))();
|
|
189
|
+
if (profile.kind === "applied" && await (deps.pickerRelayAvailable ?? livePickerRelayAvailable)(profile.proxyUrl)) {
|
|
190
|
+
return { kind: "not-applicable" };
|
|
191
|
+
}
|
|
192
|
+
} catch { // no-excuse-ok: catch -- missing profile/relay evidence must not hide a registry conflict.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
149
195
|
return assessDesktopSystemProxy({
|
|
150
196
|
platform,
|
|
151
197
|
firstParty,
|