@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { createClineIO, ClineTransactionError } from "./cline-io";
|
|
14
|
-
import {
|
|
14
|
+
import { serializeClineDocument, preserveClineSelection } from "./cline-document";
|
|
15
15
|
import { dirname } from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import type { ExportModel, ManagedContribution } from "../clients/config-export";
|
|
17
17
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
18
|
+
import { detachedConfigSnapshot } from "../config/admitted-identity";
|
|
19
|
+
import { copyPlainData } from "../lib/plain-data";
|
|
18
20
|
import type { OcxConfig } from "../types";
|
|
19
|
-
import {
|
|
21
|
+
import { defaultIntegrationIO, loadTarget, type IntegrationIO } from "./config-io";
|
|
20
22
|
import {
|
|
21
23
|
fingerprint,
|
|
22
24
|
canonicalContribution,
|
|
@@ -31,11 +33,13 @@ import {
|
|
|
31
33
|
} from "./ownership-policy";
|
|
32
34
|
import { AmbiguousSelectorError, createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
33
35
|
import { INTEGRATION_CLIENTS, isLoopbackOnly, resolveIntegrationPaths, type IntegrationClientId } from "./registry";
|
|
34
|
-
import {
|
|
36
|
+
import { declaredIntegrationTarget } from "./target";
|
|
37
|
+
import { exportContextOf } from "./state";
|
|
35
38
|
import type { IntegrationState } from "./state";
|
|
36
39
|
import { serializeDocument, UnserializableValueError } from "./serialize";
|
|
37
40
|
import { ClientPathError } from "../clients/config-export";
|
|
38
41
|
import { matchesOperationResult, newOpId, type JournalEntry } from "./journal";
|
|
42
|
+
import { observeIntegration, type IntegrationWriteInput, type RefusalReason } from "./mutation-plan";
|
|
39
43
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
40
44
|
import {
|
|
41
45
|
patchYamlFragmentSource,
|
|
@@ -62,14 +66,12 @@ function yamlRefusalReason(
|
|
|
62
66
|
}
|
|
63
67
|
import { withIntegrationWriterLock, type IntegrationWriterLockSeams } from "./writer-lock";
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| "snapshot_expired"
|
|
72
|
-
| "write_failed";
|
|
69
|
+
/**
|
|
70
|
+
* Owned by the planner so a plan can report a refusal without depending on the writer, and
|
|
71
|
+
* re-exported here because this module was its original home and every caller imports it from
|
|
72
|
+
* the writer.
|
|
73
|
+
*/
|
|
74
|
+
export type { IntegrationWriteInput, RefusalReason };
|
|
73
75
|
|
|
74
76
|
export interface WriteOk {
|
|
75
77
|
ok: true;
|
|
@@ -94,19 +96,6 @@ export interface WriteRefused {
|
|
|
94
96
|
|
|
95
97
|
export type WriteOutcome = WriteOk | WriteRefused;
|
|
96
98
|
|
|
97
|
-
export interface IntegrationWriteInput {
|
|
98
|
-
clientId: IntegrationClientId;
|
|
99
|
-
models: readonly ExportModel[];
|
|
100
|
-
config: OcxConfig;
|
|
101
|
-
port: number;
|
|
102
|
-
env?: NodeJS.ProcessEnv;
|
|
103
|
-
home?: string;
|
|
104
|
-
store?: IntegrationStateStore;
|
|
105
|
-
io?: IntegrationIO;
|
|
106
|
-
/** Frozen once by the async coordinator; synchronous callers may omit it. */
|
|
107
|
-
resolvedPaths?: { configPath: string; detectDir: string };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
99
|
export interface IntegrationRestoreInput extends IntegrationWriteInput {
|
|
111
100
|
opId: string;
|
|
112
101
|
confirmDrift?: boolean;
|
|
@@ -227,76 +216,20 @@ function sourcePreservingFragmentValue(
|
|
|
227
216
|
return fragment?.value;
|
|
228
217
|
}
|
|
229
218
|
|
|
230
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* Mutation's view of the shared observation.
|
|
221
|
+
*
|
|
222
|
+
* The read, parse and classify live in the planner so a preview and the mutation it authorizes
|
|
223
|
+
* cannot disagree. Mutation keeps the effects a preview must not have: pending-prune maintenance
|
|
224
|
+
* and Cline transaction recovery both write. Translating the planner's refusal into WriteRefused
|
|
225
|
+
* here keeps the planner free of any dependency on this module's result type.
|
|
226
|
+
*/
|
|
231
227
|
function preflight(input: IntegrationWriteInput) {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
/*
|
|
238
|
-
* Resolution itself can refuse: a relative OPENCLAW_* selector is rejected
|
|
239
|
-
* because we cannot know the gateway's working directory. That is a refusal
|
|
240
|
-
* about the user's configuration, not an internal fault, so it must not
|
|
241
|
-
* escape as an exception — the collection route would answer 500 for the
|
|
242
|
-
* whole Integrations page because one client is misconfigured.
|
|
243
|
-
*/
|
|
244
|
-
let configPath: string;
|
|
245
|
-
let detectDir: string;
|
|
246
|
-
try {
|
|
247
|
-
/*
|
|
248
|
-
* Resolve the PAIR, never one half.
|
|
249
|
-
*
|
|
250
|
-
* The coordinated path hands us a frozen pair, but applyIntegration,
|
|
251
|
-
* refreshIntegration and disableIntegration are public and may be called
|
|
252
|
-
* without one. Resolving configPath here and detectDir separately later let
|
|
253
|
-
* an Aside account switch land between the two, so a direct apply could
|
|
254
|
-
* verify account 1 was installed and then write account 0's catalog.
|
|
255
|
-
*/
|
|
256
|
-
const resolved = input.resolvedPaths ?? resolveIntegrationPaths(clientId, input.env, input.home);
|
|
257
|
-
configPath = resolved.configPath;
|
|
258
|
-
detectDir = resolved.detectDir;
|
|
259
|
-
if (clientId === "cline") io = createClineIO(io, configPath, store, true);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
if (error instanceof ClineTransactionError) {
|
|
262
|
-
return { failed: { ...refuse(clientId, "unsafe", "unsafe", error.message, error.snapshotPath), residual: true } } as const;
|
|
263
|
-
}
|
|
264
|
-
if (!(error instanceof ClientPathError)) throw error;
|
|
265
|
-
return { failed: refuse(clientId, "unsafe", "unsafe", error.message) } as const;
|
|
266
|
-
}
|
|
267
|
-
store.retryPendingPrunes();
|
|
268
|
-
|
|
269
|
-
const target = loadTarget(io, configPath);
|
|
270
|
-
if (!target.ok) {
|
|
271
|
-
return {
|
|
272
|
-
failed: refuse(clientId, "unsafe", "unsafe",
|
|
273
|
-
target.why === "read-failed"
|
|
274
|
-
? `${configPath} exists but could not be read`
|
|
275
|
-
: `${configPath} is not a regular file`),
|
|
276
|
-
} as const;
|
|
277
|
-
}
|
|
278
|
-
const before = target.before;
|
|
279
|
-
const parsed = clientId === "cline" ? parseClineDocument(before) : parseConfig(before, exportSpec.format);
|
|
280
|
-
if (parsed === PARSE_FAILED) {
|
|
281
|
-
return { failed: refuse(clientId, "unsafe", "unsafe",
|
|
282
|
-
`${configPath} could not be parsed, or holds something opencodex cannot rewrite without changing it (a non-finite number, a large integer or a tiny one a rewrite would round, -0, a duplicate member, or nesting deeper than 1000 levels)`) } as const;
|
|
283
|
-
}
|
|
284
|
-
const contribution = exportSpec.buildContribution(exportContextOf(input));
|
|
285
|
-
// A record proves ownership of the file it was written FOR. Matching only by
|
|
286
|
-
// client id let a record for one home authorize a write to another whose
|
|
287
|
-
// bytes happened to hash the same — which deleted a config we never touched.
|
|
288
|
-
const stored = store.readRecords()[clientId] ?? null;
|
|
289
|
-
const record = stored && stored.clientId === clientId && stored.configPath === configPath
|
|
290
|
-
? stored
|
|
291
|
-
: null;
|
|
292
|
-
// `configPath`/`clientId` are load-bearing, not decoration: a record proves
|
|
293
|
-
// ownership of ONE file, and the writer mutates whatever path resolves NOW.
|
|
294
|
-
// Without them a record written for another home directory would grant
|
|
295
|
-
// ownership here and disable would delete fragments it never wrote.
|
|
296
|
-
const classified = classifyIntegration({
|
|
297
|
-
fileText: before, fileIsRegular: true, parsed, record, contribution, configPath, clientId,
|
|
298
|
-
});
|
|
299
|
-
return { failed: undefined, store, io, clientId, spec, exportSpec, configPath, detectDir, before, parsed, contribution, record, classified } as const;
|
|
228
|
+
const observed = observeIntegration(input, { maintenance: true, recover: true });
|
|
229
|
+
if (!observed.failed) return observed;
|
|
230
|
+
const { reason, state, message, snapshotPath, residual } = observed.failed;
|
|
231
|
+
const refused = refuse(input.clientId, reason, state, message, snapshotPath);
|
|
232
|
+
return { failed: residual ? { ...refused, residual: true } : refused } as const;
|
|
300
233
|
}
|
|
301
234
|
|
|
302
235
|
/**
|
|
@@ -319,7 +252,7 @@ function applyOrRefreshIntegration(
|
|
|
319
252
|
): WriteOutcome {
|
|
320
253
|
const pre = preflight(input);
|
|
321
254
|
if (pre.failed) return pre.failed;
|
|
322
|
-
const { store, io, clientId, spec,
|
|
255
|
+
const { store, io, clientId, spec, target, configPath, detectDir, before, parsed, contribution, record, classified } = pre;
|
|
323
256
|
|
|
324
257
|
// The detect directory preflight already resolved, so it cannot name a
|
|
325
258
|
// different account than the config path this operation is about to write.
|
|
@@ -330,6 +263,35 @@ function applyOrRefreshIntegration(
|
|
|
330
263
|
return refuse(clientId, "non_loopback", classified.state,
|
|
331
264
|
`The generated ${clientId} integration is loopback-only and does not emit the admission header a non-loopback bind requires. Give it loopback access instead, through a tunnel or a local forwarder.`);
|
|
332
265
|
}
|
|
266
|
+
/*
|
|
267
|
+
* The write would land, and nothing would read it.
|
|
268
|
+
*
|
|
269
|
+
* This is deliberately a refusal rather than a warning attached to a success.
|
|
270
|
+
* The file is writable, our block merges cleanly, and the ownership record
|
|
271
|
+
* that follows would describe a real state of a real file — which is exactly
|
|
272
|
+
* how the original defect stayed invisible: every check the integration runs
|
|
273
|
+
* passed, the journal recorded a correct apply, and no model ever appeared in
|
|
274
|
+
* the client (#5348). Reporting the operation as done is the part that is
|
|
275
|
+
* wrong, so the operation does not report at all.
|
|
276
|
+
*
|
|
277
|
+
* Reached only when the store cannot be written: either this project's block
|
|
278
|
+
* is still in the config file, where removing it is the way forward, or the
|
|
279
|
+
* store is not a document whose shape has been observed. Where the store can
|
|
280
|
+
* be written the operation targets it and never arrives here.
|
|
281
|
+
*
|
|
282
|
+
* Apply, overwrite and refresh only. Disable removes bytes this project put
|
|
283
|
+
* in this file, and that removal is as effective as it ever was.
|
|
284
|
+
*/
|
|
285
|
+
if (target.ineffective !== null) {
|
|
286
|
+
const readsFrom = target.ineffective.store;
|
|
287
|
+
const fallback = `Add the proxy as a provider in ${clientId}'s own settings instead; \`ocx export --client ${clientId}\` prints the model list to copy.`;
|
|
288
|
+
return refuse(clientId, "superseded_store", classified.state,
|
|
289
|
+
target.ineffective.why === "owned-config-file"
|
|
290
|
+
? `${clientId} now reads its providers from ${readsFrom}, and opencodex still has a block in ${configPath}, which it no longer reads. Disable the ${clientId} integration to remove that block, then enable it again to write ${readsFrom}.`
|
|
291
|
+
: readsFrom === configPath
|
|
292
|
+
? `opencodex does not recognise the schema of ${readsFrom}, the file ${clientId} reads its providers from, so it will not merge into it. ${fallback}`
|
|
293
|
+
: `${clientId} now reads its providers from ${readsFrom}, whose schema opencodex does not recognise, so writing ${configPath} would change nothing it loads. ${fallback}`);
|
|
294
|
+
}
|
|
333
295
|
if (classified.state === "conflict") {
|
|
334
296
|
if (conflictPolicy === "refuse") {
|
|
335
297
|
return refuse(clientId, "conflict", "conflict",
|
|
@@ -440,7 +402,7 @@ function applyOrRefreshIntegration(
|
|
|
440
402
|
}
|
|
441
403
|
text = patched;
|
|
442
404
|
} else {
|
|
443
|
-
text = clientId === "cline" ? serializeClineDocument(nextDocument) : serializeDocument(nextDocument,
|
|
405
|
+
text = clientId === "cline" ? serializeClineDocument(nextDocument) : serializeDocument(nextDocument, target.format);
|
|
444
406
|
}
|
|
445
407
|
} catch (error) {
|
|
446
408
|
if (error instanceof AmbiguousSelectorError) {
|
|
@@ -528,7 +490,7 @@ export function refreshIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
528
490
|
export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
529
491
|
const pre = preflight(input);
|
|
530
492
|
if (pre.failed) return pre.failed;
|
|
531
|
-
const { store, io, clientId, spec,
|
|
493
|
+
const { store, io, clientId, spec, target, configPath, before, parsed, record, classified } = pre;
|
|
532
494
|
|
|
533
495
|
if (classified.state === "absent") {
|
|
534
496
|
return { ok: true, changed: false, state: "absent", clientId, message: "not applied" };
|
|
@@ -600,7 +562,7 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
600
562
|
}
|
|
601
563
|
text = patched;
|
|
602
564
|
} else {
|
|
603
|
-
text = clientId === "cline" ? serializeClineDocument(doc) : serializeDocument(doc,
|
|
565
|
+
text = clientId === "cline" ? serializeClineDocument(doc) : serializeDocument(doc, target.format);
|
|
604
566
|
}
|
|
605
567
|
} catch (error) {
|
|
606
568
|
if (!(error instanceof UnserializableValueError)) throw error;
|
|
@@ -638,13 +600,21 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
638
600
|
const clientId = entry.clientId;
|
|
639
601
|
const resolvedPath = input.resolvedPaths?.configPath
|
|
640
602
|
?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
|
|
641
|
-
// Restore acts on the path the operation was journaled against. Resolving a
|
|
642
|
-
// different path here would let an operation recorded for one home delete a
|
|
643
|
-
// file in another.
|
|
644
603
|
const configPath = entry.configPath;
|
|
645
|
-
|
|
604
|
+
/*
|
|
605
|
+
* Restore acts on the path the operation was journaled against. Resolving a
|
|
606
|
+
* different path here would let an operation recorded for one home delete a
|
|
607
|
+
* file in another — but a client may legally have written more than one file,
|
|
608
|
+
* so the question is whether this client still names that location, not
|
|
609
|
+
* whether it is the config file. The answer also carries the contribution
|
|
610
|
+
* shape those bytes are in, which is what the state below is measured against.
|
|
611
|
+
*/
|
|
612
|
+
const rowTarget = declaredIntegrationTarget({
|
|
613
|
+
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
|
|
614
|
+
});
|
|
615
|
+
if (rowTarget === null) {
|
|
646
616
|
return refuse(clientId, "conflict", "conflict",
|
|
647
|
-
`that operation was recorded for ${configPath},
|
|
617
|
+
`that operation was recorded for ${configPath}, which this client no longer writes; it now resolves to ${resolvedPath}`);
|
|
648
618
|
}
|
|
649
619
|
if (clientId === "cline") {
|
|
650
620
|
try { io = createClineIO(io, configPath, store, true); }
|
|
@@ -693,7 +663,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
693
663
|
// exact bytes when the snapshot was taken. Re-deriving it from the file would
|
|
694
664
|
// mean guessing which entries are ours, and a wrong guess deletes a user's.
|
|
695
665
|
const restoredRecord = entry.priorRecord;
|
|
696
|
-
const fresh =
|
|
666
|
+
const fresh = rowTarget.buildContribution(exportContextOf(input));
|
|
697
667
|
/*
|
|
698
668
|
* Does the restored record actually describe the restored bytes?
|
|
699
669
|
*
|
|
@@ -748,6 +718,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
748
718
|
|
|
749
719
|
export interface CoordinatedIntegrationOptions {
|
|
750
720
|
lockSeams?: IntegrationWriterLockSeams;
|
|
721
|
+
/**
|
|
722
|
+
* Checked after the input is frozen and the writer lock is held, before any side effect.
|
|
723
|
+
*
|
|
724
|
+
* A confirmation is a statement about state the operator was shown. Checking it out here, where
|
|
725
|
+
* the lock already excludes cooperating writers, is what makes it a decision about the same
|
|
726
|
+
* state the mutation is about to change rather than about state from a moment earlier. A
|
|
727
|
+
* non-null result refuses without writing anything.
|
|
728
|
+
*/
|
|
729
|
+
revalidate?: (frozen: IntegrationWriteInput) => Promise<WriteOutcome | null>;
|
|
751
730
|
}
|
|
752
731
|
|
|
753
732
|
/** Freeze all mutable resolution seams before the first lock await. */
|
|
@@ -759,6 +738,16 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
|
|
|
759
738
|
resolvedPaths: { configPath: string; detectDir: string };
|
|
760
739
|
};
|
|
761
740
|
|
|
741
|
+
/**
|
|
742
|
+
* An input this write cannot hold still.
|
|
743
|
+
*
|
|
744
|
+
* A coordinated write checks a plan and then writes a document from the same input, with an await
|
|
745
|
+
* in between. Anything it cannot copy would have to be read from the caller's object twice, and
|
|
746
|
+
* the second read is not the one that was checked. Refusing is bounded and says so; carrying the
|
|
747
|
+
* reference and calling it a frozen input would not be.
|
|
748
|
+
*/
|
|
749
|
+
class UncopyableIntegrationInputError extends Error {}
|
|
750
|
+
|
|
762
751
|
function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegrationInput {
|
|
763
752
|
const env = { ...(input.env ?? process.env) };
|
|
764
753
|
const home = input.home ?? homedir();
|
|
@@ -773,7 +762,23 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
773
762
|
const resolvedPaths = input.resolvedPaths
|
|
774
763
|
? { ...input.resolvedPaths }
|
|
775
764
|
: resolveIntegrationPaths(input.clientId, env, home);
|
|
776
|
-
|
|
765
|
+
/*
|
|
766
|
+
* The configuration and the roster are seams like the others, and they were the two still held
|
|
767
|
+
* by reference. A coordinated write plans from this input, awaits the writer lock and a
|
|
768
|
+
* revalidation, and only then serializes the document from it. A management route editing the
|
|
769
|
+
* live configuration, or a caller editing the model objects it passed in, would have been
|
|
770
|
+
* checked in one state and written from another, which is the substitution the fingerprint
|
|
771
|
+
* exists to prevent. Copying both here gives the plan and the document one input.
|
|
772
|
+
*/
|
|
773
|
+
const config = detachedConfigSnapshot(input.config);
|
|
774
|
+
if (config === null) {
|
|
775
|
+
throw new UncopyableIntegrationInputError("the proxy configuration could not be captured for this write");
|
|
776
|
+
}
|
|
777
|
+
const models = copyPlainData(input.models);
|
|
778
|
+
if (!models.ok) {
|
|
779
|
+
throw new UncopyableIntegrationInputError("the model roster could not be captured for this write");
|
|
780
|
+
}
|
|
781
|
+
return { ...input, config, models: models.value, env, home, store, io, resolvedPaths };
|
|
777
782
|
}
|
|
778
783
|
|
|
779
784
|
function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
@@ -782,6 +787,9 @@ function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
|
782
787
|
try {
|
|
783
788
|
return { ok: true, value: freezeIntegrationInput(input) };
|
|
784
789
|
} catch (error) {
|
|
790
|
+
if (error instanceof UncopyableIntegrationInputError) {
|
|
791
|
+
return { ok: false, refusal: refuse(input.clientId, "unsafe", "unsafe", error.message) };
|
|
792
|
+
}
|
|
785
793
|
if (!(error instanceof ClientPathError)) throw error;
|
|
786
794
|
return {
|
|
787
795
|
ok: false,
|
|
@@ -799,15 +807,22 @@ async function coordinatedWrite(
|
|
|
799
807
|
if (!prepared.ok) return prepared.refusal;
|
|
800
808
|
const frozen = prepared.value;
|
|
801
809
|
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
802
|
-
if (!spec.writerLock)
|
|
810
|
+
if (!spec.writerLock) {
|
|
811
|
+
const refused = await options?.revalidate?.(frozen);
|
|
812
|
+
return refused ?? operation(frozen);
|
|
813
|
+
}
|
|
803
814
|
|
|
804
815
|
// An absent client home is not created merely to acquire a sibling lock.
|
|
805
816
|
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
806
|
-
|
|
817
|
+
const refused = await options?.revalidate?.(frozen);
|
|
818
|
+
return refused ?? operation(frozen);
|
|
807
819
|
}
|
|
808
820
|
return withIntegrationWriterLock(
|
|
809
821
|
frozen.resolvedPaths.configPath,
|
|
810
|
-
async () =>
|
|
822
|
+
async () => {
|
|
823
|
+
const refused = await options?.revalidate?.(frozen);
|
|
824
|
+
return refused ?? operation(frozen);
|
|
825
|
+
},
|
|
811
826
|
options?.lockSeams,
|
|
812
827
|
spec.writerLock.suffix,
|
|
813
828
|
);
|
|
@@ -849,7 +864,11 @@ export async function restoreIntegrationCoordinated(
|
|
|
849
864
|
if (!prepared.ok) return prepared.refusal;
|
|
850
865
|
const frozen = prepared.value;
|
|
851
866
|
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
852
|
-
|
|
867
|
+
const run = () => restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift });
|
|
868
|
+
if (!spec.writerLock) {
|
|
869
|
+
const refused = await options?.revalidate?.(frozen);
|
|
870
|
+
return refused ?? run();
|
|
871
|
+
}
|
|
853
872
|
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
854
873
|
return refuse(
|
|
855
874
|
frozen.clientId,
|
|
@@ -860,7 +879,12 @@ export async function restoreIntegrationCoordinated(
|
|
|
860
879
|
}
|
|
861
880
|
return withIntegrationWriterLock(
|
|
862
881
|
frozen.resolvedPaths.configPath,
|
|
863
|
-
async () =>
|
|
882
|
+
async () => {
|
|
883
|
+
// An undo is bound like any other confirmation, and this is the only place where that check
|
|
884
|
+
// happens with the lock held and before the snapshot, the write and the journal row.
|
|
885
|
+
const refused = await options?.revalidate?.(frozen);
|
|
886
|
+
return refused ?? run();
|
|
887
|
+
},
|
|
864
888
|
options?.lockSeams,
|
|
865
889
|
spec.writerLock.suffix,
|
|
866
890
|
);
|
|
@@ -12,6 +12,11 @@ export function fixtureProviderConfig(adapter: string): OcxProviderConfig {
|
|
|
12
12
|
models: ["fixture-model"],
|
|
13
13
|
defaultModel: "fixture-model",
|
|
14
14
|
liveModels: false,
|
|
15
|
+
// The wire role is no longer read from the hostname, and an undeclared destination folds
|
|
16
|
+
// `developer` to `system` because one that rejects the role answers 400 and the turn never
|
|
17
|
+
// starts. This fixture is the one place that asserts the forwarded role, so the destination
|
|
18
|
+
// it stands for records that it accepts it. Only the Chat adapter reaches that decision.
|
|
19
|
+
...(adapter === "openai-chat" ? { foldDeveloperRoleToSystem: false } : {}),
|
|
15
20
|
};
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -104,6 +104,10 @@ export function classifyTransportError(error: unknown): { classification: Failur
|
|
|
104
104
|
case "network_blocked": return { classification: "network_failure", secondaryCode: code };
|
|
105
105
|
case "region_blocked": return { classification: "region_blocked", secondaryCode: code };
|
|
106
106
|
case "provider_transient": return { classification: "provider_transient", secondaryCode: code };
|
|
107
|
+
// A response this transport cannot read is an upstream protocol failure. It is deterministic
|
|
108
|
+
// rather than transient, and attributing it to the harness would blame the runner for what
|
|
109
|
+
// the peer sent.
|
|
110
|
+
case "unreadable_response": return { classification: "protocol_failure", secondaryCode: code };
|
|
107
111
|
case "connect_timeout": case "first_byte_timeout": case "total_timeout":
|
|
108
112
|
return { classification: "timeout", secondaryCode: code };
|
|
109
113
|
case "inactivity_timeout":
|
package/src/lab/live/types.ts
CHANGED
|
@@ -89,6 +89,11 @@ export type TransportErrorCode =
|
|
|
89
89
|
| "output_byte_limit"
|
|
90
90
|
| "output_token_limit"
|
|
91
91
|
| "tool_call_limit"
|
|
92
|
+
// The peer answered, and the answer cannot be read: a content coding this transport cannot
|
|
93
|
+
// undo, or coded bytes that did not decode. Neither is a timeout, a budget, or a fault of
|
|
94
|
+
// the harness, so none of those codes describes it and every one of them would send an
|
|
95
|
+
// operator looking in the wrong place.
|
|
96
|
+
| "unreadable_response"
|
|
92
97
|
| "artifact_byte_limit"
|
|
93
98
|
| "memory_limit"
|
|
94
99
|
| "child_process_limit"
|
|
@@ -9,7 +9,7 @@ const CLOSED_KEYS = new Set([
|
|
|
9
9
|
"limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
|
|
10
10
|
"modalities.input",
|
|
11
11
|
"sampling.omitTemperature", "sampling.omitTopP", "sampling.omitPenalties",
|
|
12
|
-
"reasoning.supported", "reasoning.efforts", "reasoning.defaultEffort", "reasoning.effortMap", "reasoning.wireFormat",
|
|
12
|
+
"reasoning.supported", "reasoning.efforts", "reasoning.effortsAuthoritative", "reasoning.defaultEffort", "reasoning.effortMap", "reasoning.wireFormat",
|
|
13
13
|
"reasoning.summaryMode", "reasoning.replayMode", "reasoning.splitMode", "reasoning.toggleMode", "reasoning.budgetMode",
|
|
14
14
|
"tools.choiceRestrictions", "tools.parallel", "tools.hostedPreference", "tools.customFreeform", "tools.builtinNameEscaping",
|
|
15
15
|
"cache.forwarding", "cache.retention", "anthropic.eofPolicy", "openai-chat.eofPolicy",
|
package/src/lib/admin-secrets.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
@@ -27,6 +27,14 @@ export function configuredAdminToken(configDir = getConfigDir(), env: NodeJS.Pro
|
|
|
27
27
|
|
|
28
28
|
export const ADMIN_TOKEN_PREFIX = "ocx_admin_";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Derive the bearer used by the OpenCode launcher for its one management read.
|
|
32
|
+
* A listener that captures this value cannot recover or replay the administrator token.
|
|
33
|
+
*/
|
|
34
|
+
export function opencodeCatalogToken(adminToken: string): string {
|
|
35
|
+
return `ocx_catalog_${createHmac("sha256", adminToken).update("opencode:/api/models:v1").digest("base64url")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
function secretTextEquals(left: string, right: string): boolean {
|
|
31
39
|
const a = Buffer.from(left);
|
|
32
40
|
const b = Buffer.from(right);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { OpenUrlResult } from "./open-url";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one sentence a terminal login says when nothing opened.
|
|
5
|
+
*
|
|
6
|
+
* Stated once because it is now said from three places — the Codex account login, the generic
|
|
7
|
+
* OAuth login and the key login — and a sentence restated three times drifts three ways. Callers
|
|
8
|
+
* that have something more specific to add append to it rather than rewriting it, so the part a
|
|
9
|
+
* user learns to recognize stays identical everywhere.
|
|
10
|
+
*/
|
|
11
|
+
export const BROWSER_LAUNCH_FAILED_NOTICE =
|
|
12
|
+
"⚠️ No browser could be opened here — open the URL above yourself.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Report a browser launch whose answer arrives after the code that started it has moved on.
|
|
16
|
+
*
|
|
17
|
+
* The OAuth controller does not await `onAuth`, so a CLI login cannot simply await the launcher
|
|
18
|
+
* there: the flow continues, and on a callback-server provider the very next thing it does is
|
|
19
|
+
* draw a readline prompt. A warning written at that moment lands on the line the user is typing
|
|
20
|
+
* on, which is worse than not warning at all.
|
|
21
|
+
*
|
|
22
|
+
* So the launch reports itself as soon as it settles, and anything that would collide with it
|
|
23
|
+
* waits on {@link BrowserLaunchReport.settled} first. The launcher answers within its own settle
|
|
24
|
+
* window, so the wait costs a fraction of a second and buys a deterministic order.
|
|
25
|
+
*/
|
|
26
|
+
export interface BrowserLaunchReport {
|
|
27
|
+
/** Adopt a launch already in flight. Its failure is reported once, when it settles. */
|
|
28
|
+
track(launch: Promise<OpenUrlResult>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Resolves once every launch tracked BEFORE this call has been reported, and immediately when
|
|
31
|
+
* none was. A login publishes one URL, so "before this call" and "at all" are the same set
|
|
32
|
+
* here; the narrower promise is the one this actually keeps.
|
|
33
|
+
*/
|
|
34
|
+
settled(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createBrowserLaunchReport(
|
|
38
|
+
warn: (message: string) => void = message => { console.warn(message); },
|
|
39
|
+
): BrowserLaunchReport {
|
|
40
|
+
let pending: Promise<void> = Promise.resolve();
|
|
41
|
+
return {
|
|
42
|
+
track(launch) {
|
|
43
|
+
pending = pending
|
|
44
|
+
.then(() => launch)
|
|
45
|
+
.then(
|
|
46
|
+
result => {
|
|
47
|
+
if (result.status !== "started") warn(`\n${BROWSER_LAUNCH_FAILED_NOTICE}`);
|
|
48
|
+
},
|
|
49
|
+
// openUrl is documented never to reject, and a launcher that did would mean the same
|
|
50
|
+
// thing as one that failed. Swallowing it here is not politeness: this chain is what
|
|
51
|
+
// `settled()` hands to a prompt, so a rejection would propagate out of a login that
|
|
52
|
+
// is still perfectly able to continue, and would go unhandled in the polling flows
|
|
53
|
+
// that do not reach that await until minutes later.
|
|
54
|
+
() => { warn(`\n${BROWSER_LAUNCH_FAILED_NOTICE}`); },
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
settled: () => pending,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/lib/bun-runtime.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { createRequire } from "node:module";
|
|
|
14
14
|
import { realpathSync } from "node:fs";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { isRealBunBinary } from "./bun-binary-validator.mjs";
|
|
17
|
+
import { isStandaloneBinary } from "./standalone";
|
|
17
18
|
|
|
18
19
|
export { isRealBunBinary };
|
|
19
20
|
|
|
@@ -38,10 +39,10 @@ export const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";
|
|
|
38
39
|
*/
|
|
39
40
|
export const BUN_RUNTIME_PATH_ENV = "OCX_BUN_RUNTIME_PATH";
|
|
40
41
|
|
|
41
|
-
export type BunRuntimeSource = "override" | "bundled" | "process";
|
|
42
|
+
export type BunRuntimeSource = "override" | "bundled" | "process" | "standalone";
|
|
42
43
|
|
|
43
44
|
/** The only provenance values any surface may accept off the wire or out of the env. */
|
|
44
|
-
export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process"];
|
|
45
|
+
export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process", "standalone"];
|
|
45
46
|
|
|
46
47
|
export type DurableBunRuntime = {
|
|
47
48
|
path: string;
|
|
@@ -165,6 +166,9 @@ function unmarkedDurableBunRuntime(): DurableBunRuntime {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export function durableBunRuntime(): DurableBunRuntime {
|
|
169
|
+
if (isStandaloneBinary()) {
|
|
170
|
+
return { path: process.execPath, source: "standalone", overrideEnv: BUN_OVERRIDE_ENV };
|
|
171
|
+
}
|
|
168
172
|
// A durable artifact must use the runtime selected BEFORE Bun auto-loaded a
|
|
169
173
|
// project dotenv. The Node launcher and owned service/shim launchers stamp the
|
|
170
174
|
// selected source/path pair; it is accepted only when it names this exact
|
|
@@ -9,7 +9,12 @@ export interface DebugLogEntry {
|
|
|
9
9
|
|
|
10
10
|
const MAX_LINES = 2_000;
|
|
11
11
|
const MAX_DEBUG_SUBSCRIBERS = 64;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Per-line retention cap. Exported because a producer of a long structured line has to budget
|
|
14
|
+
* BELOW it: this buffer truncates at a byte boundary, so a JSON line cut here stops being
|
|
15
|
+
* parseable while its retained prefix still reads as complete.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_DEBUG_LINE_BYTES = 16 * 1024;
|
|
13
18
|
const buffer: DebugLogEntry[] = [];
|
|
14
19
|
interface DebugSubscriberRegistration { lease: AdmissionLease }
|
|
15
20
|
const listeners = new Map<(entry: DebugLogEntry) => void, DebugSubscriberRegistration>();
|
package/src/lib/debug.ts
CHANGED
|
@@ -29,3 +29,66 @@ export function debugProviderDiagnostic(adapter: string, event: string, details:
|
|
|
29
29
|
/* diagnostics must never affect request handling */
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The same diagnostic for details that are expensive to compute or read live request state.
|
|
35
|
+
*
|
|
36
|
+
* The eager form takes an already-built object, so the build runs in ARGUMENT position: outside
|
|
37
|
+
* this module's gate, and outside its try/catch. For a projection that walks a whole request
|
|
38
|
+
* that is wrong twice over — it pays the walk while debug is off, and a throw inside it reaches
|
|
39
|
+
* the caller, which for an adapter means a built request becomes a rejected one and a send that
|
|
40
|
+
* would have happened does not. Pass the builder instead: it runs only when debug is on, and a
|
|
41
|
+
* throw inside it is swallowed exactly like a throw in the emit.
|
|
42
|
+
*/
|
|
43
|
+
export function debugProviderDiagnosticLazy(
|
|
44
|
+
adapter: string,
|
|
45
|
+
event: string,
|
|
46
|
+
details: () => Record<string, unknown>,
|
|
47
|
+
): void {
|
|
48
|
+
if (!isDebugEnabled()) return;
|
|
49
|
+
try {
|
|
50
|
+
emitDebugLine(`[ocx:${adapter}:${event}] ${JSON.stringify(redactSecrets(details()))}`);
|
|
51
|
+
} catch {
|
|
52
|
+
/* diagnostics must never affect request handling */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One line per finalized attempt, formatted from what the recorder already counted.
|
|
58
|
+
*
|
|
59
|
+
* #3983 wanted this visibility and emitted a line per stream event to get it. Two things made
|
|
60
|
+
* that the wrong shape. It is a second record: `emitDebugLine` writes the ring AND stderr, and
|
|
61
|
+
* a service manager redirects stderr to a file, so an installed service accumulates a per-event
|
|
62
|
+
* history beside the ledger with its own retention and sequencing. And per-event lines needed a
|
|
63
|
+
* per-payload fingerprint to correlate, which under a process-global key makes every repeated
|
|
64
|
+
* prompt fragment and tool name correlatable for the life of the process.
|
|
65
|
+
*
|
|
66
|
+
* So this writes the ring ONLY -- `appendDebugLogLine` directly, never `emitDebugLine` -- and
|
|
67
|
+
* says nothing the ledger does not already hold. The ring becomes a live view of the durable
|
|
68
|
+
* record rather than a parallel source for it.
|
|
69
|
+
*/
|
|
70
|
+
export function debugAttemptDeliverySummary(
|
|
71
|
+
requestId: string,
|
|
72
|
+
attempt: {
|
|
73
|
+
ordinal: number;
|
|
74
|
+
adapter: string;
|
|
75
|
+
deliverySummary?: {
|
|
76
|
+
adapterEvents: number;
|
|
77
|
+
relayedEvents: number;
|
|
78
|
+
semanticBytes: number;
|
|
79
|
+
sideEffectEvents: number;
|
|
80
|
+
terminalEvents: number;
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
): void {
|
|
84
|
+
if (!isDebugEnabled() || !attempt.deliverySummary) return;
|
|
85
|
+
try {
|
|
86
|
+
appendDebugLogLine(`[ocx:${attempt.adapter}:delivery] ${JSON.stringify({
|
|
87
|
+
requestId,
|
|
88
|
+
ordinal: attempt.ordinal,
|
|
89
|
+
...attempt.deliverySummary,
|
|
90
|
+
})}`);
|
|
91
|
+
} catch {
|
|
92
|
+
/* diagnostics must never affect request handling */
|
|
93
|
+
}
|
|
94
|
+
}
|