@bitkyc08/opencodex 2.86.0 → 2.87.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-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.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.ts +108 -21
- 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.ts +35 -8
- 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 +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -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 +11 -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 +38 -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 +17 -14
- 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/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 +12 -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/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 +52 -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 +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- 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/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 +33 -7
- 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 +5 -4
- 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/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 +24 -2
- 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/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 +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -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 +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- 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 +101 -21
- 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 +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -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 +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- 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/request-prepare.ts +76 -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 +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- 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 +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -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 +22 -9
- 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/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { anthropicProgressItems } from "../responses/anthropic-progress";
|
|
1
2
|
import type {
|
|
2
3
|
AdapterEvent,
|
|
3
4
|
OcxMessagePhase,
|
|
@@ -67,6 +68,8 @@ export function buildResponseJSON(
|
|
|
67
68
|
if (options?.translatorBudget && options.recordBufferedDelivery !== false) {
|
|
68
69
|
attemptDeliveryRecorder(budget)?.noteBufferedDelivery(body);
|
|
69
70
|
}
|
|
71
|
+
if (body.status === "completed" && options?.compaction
|
|
72
|
+
&& !events.some(event => event.type === "done" && event.compactionEncryptedContent)) options.compactionRetention?.commit();
|
|
70
73
|
return body;
|
|
71
74
|
} finally {
|
|
72
75
|
for (const event of events) {
|
|
@@ -98,6 +101,7 @@ function buildResponseJSONWithBudget(
|
|
|
98
101
|
toolSearchToolNames?: Set<string>;
|
|
99
102
|
/** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
|
|
100
103
|
compaction?: boolean;
|
|
104
|
+
compactionRetention?: { encode(summary: string): string; commit(): void };
|
|
101
105
|
onProviderState?: (state: OcxProviderContinuationState) => void;
|
|
102
106
|
/** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
|
|
103
107
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
@@ -386,6 +390,16 @@ function buildResponseJSONWithBudget(
|
|
|
386
390
|
flushToolCall();
|
|
387
391
|
break;
|
|
388
392
|
case "text_delta":
|
|
393
|
+
if (options?.compaction && e.anthropicThinkingSignature) break;
|
|
394
|
+
if (e.anthropicThinkingSignature) {
|
|
395
|
+
rawReasoningForNextToolCall = "";
|
|
396
|
+
flushText("commentary");
|
|
397
|
+
flushSummaryReasoning();
|
|
398
|
+
flushRawReasoning();
|
|
399
|
+
flushToolCall();
|
|
400
|
+
for (const item of anthropicProgressItems(e.text, e.anthropicThinkingSignature, budget)) pushOutput(item as OutputItem);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
389
403
|
// Only flush on an explicit phase change. A later delta that omits `phase` must keep
|
|
390
404
|
// appending under the previously established phase.
|
|
391
405
|
if (currentText.bytes > 0 && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
|
|
@@ -533,8 +547,9 @@ function buildResponseJSONWithBudget(
|
|
|
533
547
|
const safeSources = safeWebSearchSources(e.sources);
|
|
534
548
|
pushOutput({
|
|
535
549
|
type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
|
|
536
|
-
action: webSearchAction(e.queries),
|
|
550
|
+
action: webSearchAction(e.queries, e.fetch),
|
|
537
551
|
...(safeSources.length > 0 ? { sources: safeSources } : {}),
|
|
552
|
+
...(e.fetch ? { fetch_result: e.fetch.result } : {}),
|
|
538
553
|
});
|
|
539
554
|
if (safeSources.length > 0) {
|
|
540
555
|
for (const source of safeSources) {
|
|
@@ -616,7 +631,7 @@ function buildResponseJSONWithBudget(
|
|
|
616
631
|
) {
|
|
617
632
|
const item = {
|
|
618
633
|
type: "compaction", id: `cmp_${uuid()}`,
|
|
619
|
-
encrypted_content: compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(batchCompaction)),
|
|
634
|
+
encrypted_content: compactionEncryptedContent ?? (options?.compactionRetention?.encode(joinChunks(batchCompaction)) ?? encodeCompactionSummary(joinChunks(batchCompaction))),
|
|
620
635
|
};
|
|
621
636
|
pushOutput(item, compactionEncryptedContent ? 0 : batchCompaction.bytes);
|
|
622
637
|
}
|
|
@@ -646,7 +661,7 @@ function buildResponseJSONWithBudget(
|
|
|
646
661
|
: errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
|
|
647
662
|
...(incompleteEvent ? {
|
|
648
663
|
incomplete_details: {
|
|
649
|
-
reason: incompleteEvent.reason,
|
|
664
|
+
reason: truncationReasonFor(incompleteEvent.reason) ?? incompleteEvent.reason,
|
|
650
665
|
...(incompleteEvent.message ? { message: incompleteEvent.message } : {}),
|
|
651
666
|
...(incompleteEvent.retryable !== undefined ? { retryable: incompleteEvent.retryable } : {}),
|
|
652
667
|
},
|
package/src/bridge/sse.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { anthropicProgressItems } from "../responses/anthropic-progress";
|
|
1
2
|
import type {
|
|
2
3
|
AdapterEvent,
|
|
3
4
|
OcxMessagePhase,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
classifyError,
|
|
13
14
|
cyberPolicyErrorType,
|
|
14
15
|
CYBER_POLICY_ERROR_CODE,
|
|
15
|
-
|
|
16
|
+
isTerminalRefusalCode,
|
|
16
17
|
type OcxErrorPayload,
|
|
17
18
|
} from "../lib/errors";
|
|
18
19
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
} from "../lib/translator-budget";
|
|
51
52
|
import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
|
|
52
53
|
import type { OutputItem, StringChunks } from "./internal";
|
|
54
|
+
import { terminalRefusalFailure } from "./terminal-refusal";
|
|
53
55
|
|
|
54
56
|
function sseEvent(name: string, data: Record<string, unknown>): string {
|
|
55
57
|
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
@@ -88,6 +90,7 @@ export function bridgeToResponsesSSE(
|
|
|
88
90
|
* response.completed — codex-rs collect_compaction_output requires exactly one.
|
|
89
91
|
*/
|
|
90
92
|
compaction?: boolean;
|
|
93
|
+
compactionRetention?: { encode(summary: string): string; commit(): void };
|
|
91
94
|
/** One-shot: first non-empty text/thinking/raw-reasoning/tool-input delta observed. */
|
|
92
95
|
onFirstOutput?: () => void;
|
|
93
96
|
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
@@ -120,6 +123,12 @@ export function bridgeToResponsesSSE(
|
|
|
120
123
|
* would reach the client under the invented name instead of the normalized one.
|
|
121
124
|
*/
|
|
122
125
|
enforceDeclaredToolNames?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Deliver a non-retryable upstream refusal as `response.failed` with a terminal-refusal code
|
|
128
|
+
* instead of `response.incomplete` (see `./terminal-refusal`). Set only for the Responses
|
|
129
|
+
* inbound wire: the chat and Anthropic encoders read the incomplete to report a filter stop.
|
|
130
|
+
*/
|
|
131
|
+
terminalRefusalAsFailed?: boolean;
|
|
123
132
|
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
124
133
|
toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
|
|
125
134
|
/**
|
|
@@ -676,12 +685,15 @@ export function bridgeToResponsesSSE(
|
|
|
676
685
|
// leaves a "Searching the web" spinner spinning forever.
|
|
677
686
|
// `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
|
|
678
687
|
// downstream translators (claude outbound) can fill web_search_tool_result content.
|
|
679
|
-
|
|
688
|
+
// `fetch` (Anthropic's server-side web_fetch) makes the cell a page open and rides its result
|
|
689
|
+
// verbatim as `fetch_result`, for the same claude outbound translator.
|
|
690
|
+
const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], fetch?: { url: string; result: unknown }) => {
|
|
680
691
|
if (!currentWebSearch) return;
|
|
681
692
|
const item = {
|
|
682
693
|
type: "web_search_call", id: currentWebSearch.itemId, status,
|
|
683
|
-
action: webSearchAction(queries),
|
|
694
|
+
action: webSearchAction(queries, fetch),
|
|
684
695
|
...(sources && sources.length > 0 ? { sources } : {}),
|
|
696
|
+
...(fetch ? { fetch_result: fetch.result } : {}),
|
|
685
697
|
};
|
|
686
698
|
emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
|
|
687
699
|
retainFinishedItem(item as OutputItem);
|
|
@@ -841,6 +853,7 @@ export function bridgeToResponsesSSE(
|
|
|
841
853
|
// its compaction UI renders nothing mid-turn, so nothing is lost visually.
|
|
842
854
|
if (options?.compaction) {
|
|
843
855
|
if (event.type === "text_delta") {
|
|
856
|
+
if (event.anthropicThinkingSignature) continue;
|
|
844
857
|
compaction = appendString(
|
|
845
858
|
compaction,
|
|
846
859
|
event.text,
|
|
@@ -871,6 +884,29 @@ export function bridgeToResponsesSSE(
|
|
|
871
884
|
break;
|
|
872
885
|
}
|
|
873
886
|
case "text_delta": {
|
|
887
|
+
if (event.anthropicThinkingSignature) {
|
|
888
|
+
rawReasoningForNextToolCall = "";
|
|
889
|
+
closeCurrentMessage("commentary");
|
|
890
|
+
closeCurrentReasoning();
|
|
891
|
+
closeCurrentRawReasoning();
|
|
892
|
+
flushHiddenRawReasoning();
|
|
893
|
+
flushHiddenReasoningEnvelope();
|
|
894
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
895
|
+
for (const item of anthropicProgressItems(event.text, event.anthropicThinkingSignature, budget)) {
|
|
896
|
+
emit("response.output_item.added", { output_index: outputIndex, item: { ...item, ...(item.type === "message" ? { status: "in_progress", content: [] } : {}) } });
|
|
897
|
+
if (item.type === "message") {
|
|
898
|
+
const part = item.content![0];
|
|
899
|
+
emit("response.content_part.added", { item_id: item.id, output_index: outputIndex, content_index: 0, part: { ...part, text: "" } });
|
|
900
|
+
emit("response.output_text.delta", { item_id: item.id, output_index: outputIndex, content_index: 0, delta: event.text });
|
|
901
|
+
emit("response.output_text.done", { item_id: item.id, output_index: outputIndex, content_index: 0, text: event.text });
|
|
902
|
+
emit("response.content_part.done", { item_id: item.id, output_index: outputIndex, content_index: 0, part });
|
|
903
|
+
}
|
|
904
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
905
|
+
retainFinishedItem(item as OutputItem);
|
|
906
|
+
outputIndex++;
|
|
907
|
+
}
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
874
910
|
if (currentReasoning) closeCurrentReasoning();
|
|
875
911
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
876
912
|
flushHiddenRawReasoning();
|
|
@@ -1195,7 +1231,7 @@ export function bridgeToResponsesSSE(
|
|
|
1195
1231
|
currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
|
|
1196
1232
|
}
|
|
1197
1233
|
const safeSources = safeWebSearchSources(event.sources);
|
|
1198
|
-
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources);
|
|
1234
|
+
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.fetch);
|
|
1199
1235
|
// Queue this search's sources for the next assistant message (dedup by URL).
|
|
1200
1236
|
if (safeSources.length > 0) {
|
|
1201
1237
|
for (const source of safeSources) {
|
|
@@ -1233,7 +1269,7 @@ export function bridgeToResponsesSSE(
|
|
|
1233
1269
|
// Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
|
|
1234
1270
|
const item = {
|
|
1235
1271
|
type: "compaction", id: `cmp_${uuid()}`,
|
|
1236
|
-
encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(compaction)),
|
|
1272
|
+
encrypted_content: event.compactionEncryptedContent ?? (options?.compactionRetention?.encode(joinChunks(compaction)) ?? encodeCompactionSummary(joinChunks(compaction))),
|
|
1237
1273
|
};
|
|
1238
1274
|
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
1239
1275
|
retainFinishedItem(item as OutputItem, event.compactionEncryptedContent ? 0 : compaction.bytes);
|
|
@@ -1271,6 +1307,7 @@ export function bridgeToResponsesSSE(
|
|
|
1271
1307
|
emit("response.completed", {
|
|
1272
1308
|
response,
|
|
1273
1309
|
});
|
|
1310
|
+
if (!closed && !terminated && options?.compaction && !event.compactionEncryptedContent) options.compactionRetention?.commit();
|
|
1274
1311
|
reportTerminal("completed");
|
|
1275
1312
|
}
|
|
1276
1313
|
terminalEvent = true;
|
|
@@ -1287,12 +1324,25 @@ export function bridgeToResponsesSSE(
|
|
|
1287
1324
|
flushHiddenReasoningEnvelope();
|
|
1288
1325
|
options?.onUsage?.(event.usage);
|
|
1289
1326
|
await awaitThoughtSignatureDurability();
|
|
1327
|
+
const refusal = options?.terminalRefusalAsFailed ? terminalRefusalFailure(event) : undefined;
|
|
1328
|
+
if (refusal) {
|
|
1329
|
+
emit("response.failed", {
|
|
1330
|
+
response: {
|
|
1331
|
+
...responseSnapshot("failed", finishedItems, event.endTurn),
|
|
1332
|
+
usage: responsesUsage(event.usage),
|
|
1333
|
+
...refusal,
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
reportTerminal("failed");
|
|
1337
|
+
terminalEvent = true;
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1290
1340
|
emit("response.incomplete", {
|
|
1291
1341
|
response: {
|
|
1292
1342
|
...responseSnapshot("incomplete", finishedItems, event.endTurn),
|
|
1293
1343
|
usage: responsesUsage(event.usage),
|
|
1294
1344
|
incomplete_details: {
|
|
1295
|
-
reason: event.reason,
|
|
1345
|
+
reason: truncationReasonFor(event.reason) ?? event.reason,
|
|
1296
1346
|
...(event.message ? { message: event.message } : {}),
|
|
1297
1347
|
...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
1298
1348
|
},
|
|
@@ -1325,7 +1375,7 @@ export function bridgeToResponsesSSE(
|
|
|
1325
1375
|
...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
|
|
1326
1376
|
error: failure.error,
|
|
1327
1377
|
last_error: failure.error,
|
|
1328
|
-
...(
|
|
1378
|
+
...(isTerminalRefusalCode(failure.error.code)
|
|
1329
1379
|
? { retryable: false }
|
|
1330
1380
|
: event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
1331
1381
|
},
|
|
@@ -1366,7 +1416,7 @@ export function bridgeToResponsesSSE(
|
|
|
1366
1416
|
...responseSnapshot("failed", finishedItems),
|
|
1367
1417
|
error: failure,
|
|
1368
1418
|
last_error: failure,
|
|
1369
|
-
...(
|
|
1419
|
+
...(isTerminalRefusalCode(failure.code) ? { retryable: false } : {}),
|
|
1370
1420
|
},
|
|
1371
1421
|
});
|
|
1372
1422
|
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");
|