@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- 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 +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- 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/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- 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/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- 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-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- 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/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- 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/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- 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-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -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 +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.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
16
|
import { EXPORT_CLIENTS, type ExportModel, type 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,12 @@ 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 { exportContextOf } from "./state";
|
|
35
37
|
import type { IntegrationState } from "./state";
|
|
36
38
|
import { serializeDocument, UnserializableValueError } from "./serialize";
|
|
37
39
|
import { ClientPathError } from "../clients/config-export";
|
|
38
40
|
import { matchesOperationResult, newOpId, type JournalEntry } from "./journal";
|
|
41
|
+
import { observeIntegration, type IntegrationWriteInput, type RefusalReason } from "./mutation-plan";
|
|
39
42
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
40
43
|
import {
|
|
41
44
|
patchYamlFragmentSource,
|
|
@@ -62,14 +65,12 @@ function yamlRefusalReason(
|
|
|
62
65
|
}
|
|
63
66
|
import { withIntegrationWriterLock, type IntegrationWriterLockSeams } from "./writer-lock";
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| "snapshot_expired"
|
|
72
|
-
| "write_failed";
|
|
68
|
+
/**
|
|
69
|
+
* Owned by the planner so a plan can report a refusal without depending on the writer, and
|
|
70
|
+
* re-exported here because this module was its original home and every caller imports it from
|
|
71
|
+
* the writer.
|
|
72
|
+
*/
|
|
73
|
+
export type { IntegrationWriteInput, RefusalReason };
|
|
73
74
|
|
|
74
75
|
export interface WriteOk {
|
|
75
76
|
ok: true;
|
|
@@ -94,19 +95,6 @@ export interface WriteRefused {
|
|
|
94
95
|
|
|
95
96
|
export type WriteOutcome = WriteOk | WriteRefused;
|
|
96
97
|
|
|
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
98
|
export interface IntegrationRestoreInput extends IntegrationWriteInput {
|
|
111
99
|
opId: string;
|
|
112
100
|
confirmDrift?: boolean;
|
|
@@ -227,76 +215,20 @@ function sourcePreservingFragmentValue(
|
|
|
227
215
|
return fragment?.value;
|
|
228
216
|
}
|
|
229
217
|
|
|
230
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Mutation's view of the shared observation.
|
|
220
|
+
*
|
|
221
|
+
* The read, parse and classify live in the planner so a preview and the mutation it authorizes
|
|
222
|
+
* cannot disagree. Mutation keeps the effects a preview must not have: pending-prune maintenance
|
|
223
|
+
* and Cline transaction recovery both write. Translating the planner's refusal into WriteRefused
|
|
224
|
+
* here keeps the planner free of any dependency on this module's result type.
|
|
225
|
+
*/
|
|
231
226
|
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;
|
|
227
|
+
const observed = observeIntegration(input, { maintenance: true, recover: true });
|
|
228
|
+
if (!observed.failed) return observed;
|
|
229
|
+
const { reason, state, message, snapshotPath, residual } = observed.failed;
|
|
230
|
+
const refused = refuse(input.clientId, reason, state, message, snapshotPath);
|
|
231
|
+
return { failed: residual ? { ...refused, residual: true } : refused } as const;
|
|
300
232
|
}
|
|
301
233
|
|
|
302
234
|
/**
|
|
@@ -748,6 +680,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
748
680
|
|
|
749
681
|
export interface CoordinatedIntegrationOptions {
|
|
750
682
|
lockSeams?: IntegrationWriterLockSeams;
|
|
683
|
+
/**
|
|
684
|
+
* Checked after the input is frozen and the writer lock is held, before any side effect.
|
|
685
|
+
*
|
|
686
|
+
* A confirmation is a statement about state the operator was shown. Checking it out here, where
|
|
687
|
+
* the lock already excludes cooperating writers, is what makes it a decision about the same
|
|
688
|
+
* state the mutation is about to change rather than about state from a moment earlier. A
|
|
689
|
+
* non-null result refuses without writing anything.
|
|
690
|
+
*/
|
|
691
|
+
revalidate?: (frozen: IntegrationWriteInput) => Promise<WriteOutcome | null>;
|
|
751
692
|
}
|
|
752
693
|
|
|
753
694
|
/** Freeze all mutable resolution seams before the first lock await. */
|
|
@@ -759,6 +700,16 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
|
|
|
759
700
|
resolvedPaths: { configPath: string; detectDir: string };
|
|
760
701
|
};
|
|
761
702
|
|
|
703
|
+
/**
|
|
704
|
+
* An input this write cannot hold still.
|
|
705
|
+
*
|
|
706
|
+
* A coordinated write checks a plan and then writes a document from the same input, with an await
|
|
707
|
+
* in between. Anything it cannot copy would have to be read from the caller's object twice, and
|
|
708
|
+
* the second read is not the one that was checked. Refusing is bounded and says so; carrying the
|
|
709
|
+
* reference and calling it a frozen input would not be.
|
|
710
|
+
*/
|
|
711
|
+
class UncopyableIntegrationInputError extends Error {}
|
|
712
|
+
|
|
762
713
|
function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegrationInput {
|
|
763
714
|
const env = { ...(input.env ?? process.env) };
|
|
764
715
|
const home = input.home ?? homedir();
|
|
@@ -773,7 +724,23 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
773
724
|
const resolvedPaths = input.resolvedPaths
|
|
774
725
|
? { ...input.resolvedPaths }
|
|
775
726
|
: resolveIntegrationPaths(input.clientId, env, home);
|
|
776
|
-
|
|
727
|
+
/*
|
|
728
|
+
* The configuration and the roster are seams like the others, and they were the two still held
|
|
729
|
+
* by reference. A coordinated write plans from this input, awaits the writer lock and a
|
|
730
|
+
* revalidation, and only then serializes the document from it. A management route editing the
|
|
731
|
+
* live configuration, or a caller editing the model objects it passed in, would have been
|
|
732
|
+
* checked in one state and written from another, which is the substitution the fingerprint
|
|
733
|
+
* exists to prevent. Copying both here gives the plan and the document one input.
|
|
734
|
+
*/
|
|
735
|
+
const config = detachedConfigSnapshot(input.config);
|
|
736
|
+
if (config === null) {
|
|
737
|
+
throw new UncopyableIntegrationInputError("the proxy configuration could not be captured for this write");
|
|
738
|
+
}
|
|
739
|
+
const models = copyPlainData(input.models);
|
|
740
|
+
if (!models.ok) {
|
|
741
|
+
throw new UncopyableIntegrationInputError("the model roster could not be captured for this write");
|
|
742
|
+
}
|
|
743
|
+
return { ...input, config, models: models.value, env, home, store, io, resolvedPaths };
|
|
777
744
|
}
|
|
778
745
|
|
|
779
746
|
function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
@@ -782,6 +749,9 @@ function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
|
782
749
|
try {
|
|
783
750
|
return { ok: true, value: freezeIntegrationInput(input) };
|
|
784
751
|
} catch (error) {
|
|
752
|
+
if (error instanceof UncopyableIntegrationInputError) {
|
|
753
|
+
return { ok: false, refusal: refuse(input.clientId, "unsafe", "unsafe", error.message) };
|
|
754
|
+
}
|
|
785
755
|
if (!(error instanceof ClientPathError)) throw error;
|
|
786
756
|
return {
|
|
787
757
|
ok: false,
|
|
@@ -799,15 +769,22 @@ async function coordinatedWrite(
|
|
|
799
769
|
if (!prepared.ok) return prepared.refusal;
|
|
800
770
|
const frozen = prepared.value;
|
|
801
771
|
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
802
|
-
if (!spec.writerLock)
|
|
772
|
+
if (!spec.writerLock) {
|
|
773
|
+
const refused = await options?.revalidate?.(frozen);
|
|
774
|
+
return refused ?? operation(frozen);
|
|
775
|
+
}
|
|
803
776
|
|
|
804
777
|
// An absent client home is not created merely to acquire a sibling lock.
|
|
805
778
|
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
806
|
-
|
|
779
|
+
const refused = await options?.revalidate?.(frozen);
|
|
780
|
+
return refused ?? operation(frozen);
|
|
807
781
|
}
|
|
808
782
|
return withIntegrationWriterLock(
|
|
809
783
|
frozen.resolvedPaths.configPath,
|
|
810
|
-
async () =>
|
|
784
|
+
async () => {
|
|
785
|
+
const refused = await options?.revalidate?.(frozen);
|
|
786
|
+
return refused ?? operation(frozen);
|
|
787
|
+
},
|
|
811
788
|
options?.lockSeams,
|
|
812
789
|
spec.writerLock.suffix,
|
|
813
790
|
);
|
|
@@ -849,7 +826,11 @@ export async function restoreIntegrationCoordinated(
|
|
|
849
826
|
if (!prepared.ok) return prepared.refusal;
|
|
850
827
|
const frozen = prepared.value;
|
|
851
828
|
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
852
|
-
|
|
829
|
+
const run = () => restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift });
|
|
830
|
+
if (!spec.writerLock) {
|
|
831
|
+
const refused = await options?.revalidate?.(frozen);
|
|
832
|
+
return refused ?? run();
|
|
833
|
+
}
|
|
853
834
|
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
854
835
|
return refuse(
|
|
855
836
|
frozen.clientId,
|
|
@@ -860,7 +841,12 @@ export async function restoreIntegrationCoordinated(
|
|
|
860
841
|
}
|
|
861
842
|
return withIntegrationWriterLock(
|
|
862
843
|
frozen.resolvedPaths.configPath,
|
|
863
|
-
async () =>
|
|
844
|
+
async () => {
|
|
845
|
+
// An undo is bound like any other confirmation, and this is the only place where that check
|
|
846
|
+
// happens with the lock held and before the snapshot, the write and the journal row.
|
|
847
|
+
const refused = await options?.revalidate?.(frozen);
|
|
848
|
+
return refused ?? run();
|
|
849
|
+
},
|
|
864
850
|
options?.lockSeams,
|
|
865
851
|
spec.writerLock.suffix,
|
|
866
852
|
);
|
|
@@ -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);
|
package/src/lib/bounded-body.ts
CHANGED
|
@@ -129,9 +129,11 @@ export async function readBoundedResponseBytes(
|
|
|
129
129
|
options: BoundedBytesOptions,
|
|
130
130
|
): Promise<BoundedBytesResult> {
|
|
131
131
|
const signal = options.signal;
|
|
132
|
-
if (signal?.aborted) throw signal.reason;
|
|
133
|
-
|
|
134
132
|
const body = response.body;
|
|
133
|
+
if (signal?.aborted) {
|
|
134
|
+
if (body) cancelBodyWithoutWaiting(body, signal.reason);
|
|
135
|
+
throw signal.reason;
|
|
136
|
+
}
|
|
135
137
|
if (!body) return { bytes: new Uint8Array(0), oversized: false };
|
|
136
138
|
|
|
137
139
|
const reader = body.getReader();
|
|
@@ -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,26 @@ 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
|
+
}
|
|
@@ -240,6 +240,43 @@ function registryAllowsPrivateNetwork(name: string): boolean {
|
|
|
240
240
|
return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
function normalizedCanonicalEndpoint(value: string): string | null {
|
|
244
|
+
try {
|
|
245
|
+
const parsed = new URL(value.trim());
|
|
246
|
+
if (parsed.username || parsed.password || parsed.hash) return null;
|
|
247
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
248
|
+
return parsed.toString().replace(/\/$/, "");
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Config-time Fake-IP allowance for the one overridable OAuth provider whose canonical
|
|
256
|
+
* Google endpoint is known to sit behind Clash/Mihomo transparent DNS in the field.
|
|
257
|
+
*
|
|
258
|
+
* Antigravity permits a custom base URL, so provider identity alone is not authority:
|
|
259
|
+
* adapter, auth mode, and the normalized final base URL must still match the registry seed.
|
|
260
|
+
* Custom destinations therefore keep the ordinary fail-closed SSRF policy.
|
|
261
|
+
*/
|
|
262
|
+
function registryAllowsBenchmarkDnsAtWriteTime(
|
|
263
|
+
name: string,
|
|
264
|
+
provider: Pick<OcxProviderConfig, "baseUrl"> & Partial<Pick<OcxProviderConfig, "adapter" | "authMode">>,
|
|
265
|
+
): boolean {
|
|
266
|
+
if (name !== "google-antigravity") return false;
|
|
267
|
+
const entry = getProviderRegistryEntry(name);
|
|
268
|
+
if (
|
|
269
|
+
!entry
|
|
270
|
+
|| entry.authKind !== "oauth"
|
|
271
|
+
|| entry.allowBaseUrlOverride !== true
|
|
272
|
+
|| provider.adapter !== entry.adapter
|
|
273
|
+
|| provider.authMode !== "oauth"
|
|
274
|
+
) return false;
|
|
275
|
+
const configured = normalizedCanonicalEndpoint(provider.baseUrl);
|
|
276
|
+
const canonical = normalizedCanonicalEndpoint(entry.baseUrl);
|
|
277
|
+
return configured !== null && configured === canonical;
|
|
278
|
+
}
|
|
279
|
+
|
|
243
280
|
/**
|
|
244
281
|
* OAuth registry entries that opt into `allowBaseUrlOverride` send bearer credentials to a
|
|
245
282
|
* user-configured endpoint (review findings, PR #2109 / PR #2110): a cleartext `http:`
|
|
@@ -328,13 +365,16 @@ export function assertProviderDestinationAllowed(name: string, provider: Pick<Oc
|
|
|
328
365
|
* advisory and must not hard-fail offline startups. DNS rebinding after validation is
|
|
329
366
|
* a recorded residual for this loopback proxy (devlog 260712_pr_batch_landing 000).
|
|
330
367
|
*
|
|
331
|
-
* `allowBenchmarkAddresses`
|
|
332
|
-
*
|
|
333
|
-
*
|
|
368
|
+
* `allowBenchmarkAddresses` remains the explicit caller opt-in used by canonical ChatGPT
|
|
369
|
+
* Codex. Canonical Google Antigravity receives the same 198.18.0.0/15 DNS-answer exception
|
|
370
|
+
* only when its adapter/auth/base URL still match the registry seed. Every other non-public
|
|
371
|
+
* answer — including custom Antigravity destinations and mixed benchmark + private/metadata
|
|
372
|
+
* sets — still fails.
|
|
334
373
|
*/
|
|
335
374
|
export async function providerDestinationResolvedError(
|
|
336
375
|
name: string,
|
|
337
|
-
provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork"
|
|
376
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">
|
|
377
|
+
& Partial<Pick<OcxProviderConfig, "adapter" | "authMode">>,
|
|
338
378
|
options?: { allowBenchmarkAddresses?: boolean },
|
|
339
379
|
): Promise<string | null> {
|
|
340
380
|
const syncError = providerDestinationConfigError(name, provider);
|
|
@@ -349,6 +389,8 @@ export async function providerDestinationResolvedError(
|
|
|
349
389
|
return null; // literals and localhost are fully handled by the sync path
|
|
350
390
|
}
|
|
351
391
|
if (providerAllowsPrivateNetwork(name, provider)) return null;
|
|
392
|
+
const allowBenchmarkAddresses = options?.allowBenchmarkAddresses === true
|
|
393
|
+
|| registryAllowsBenchmarkDnsAtWriteTime(name, provider);
|
|
352
394
|
let addresses: { address: string }[];
|
|
353
395
|
try {
|
|
354
396
|
addresses = await lookup(hostname, { all: true, verbatim: true });
|
|
@@ -360,7 +402,7 @@ export async function providerDestinationResolvedError(
|
|
|
360
402
|
const assessment = ipKind === 4 ? classifyIpv4(address) : ipKind === 6 ? classifyIpv6(normalizeHostname(address)) : null;
|
|
361
403
|
if (!assessment || assessment.kind === "public") continue;
|
|
362
404
|
// Clash fake-IP only: 198.18/19 benchmark detail. Mixed dangerous sets still reject.
|
|
363
|
-
if (
|
|
405
|
+
if (allowBenchmarkAddresses && isBenchmarkDnsAnswer(address, assessment)) {
|
|
364
406
|
continue;
|
|
365
407
|
}
|
|
366
408
|
if (assessment.kind === "metadata") return `baseUrl hostname ${hostname} resolves to a blocked metadata endpoint (${address})`;
|
|
@@ -492,4 +534,4 @@ export async function resolvePublicAddresses(
|
|
|
492
534
|
*/
|
|
493
535
|
export async function assertUrlResolvesPublic(url: string): Promise<void> {
|
|
494
536
|
await resolvePublicAddresses(url);
|
|
495
|
-
}
|
|
537
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseRetryAfterFromMessage } from "./retry-delay";
|
|
2
|
+
|
|
1
3
|
export interface OcxErrorPayload {
|
|
2
4
|
message: string;
|
|
3
5
|
type: string;
|
|
@@ -72,6 +74,75 @@ export function isCyberPolicyMessage(text: string): boolean {
|
|
|
72
74
|
return false;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Refusal codes Codex ends a turn on.
|
|
79
|
+
*
|
|
80
|
+
* Its Responses parser classifies a `response.failed` terminal by `error.code`
|
|
81
|
+
* alone (codex-rs/codex-api/src/sse/responses.rs:423-462). These four become a
|
|
82
|
+
* fatal `ApiError` the client reports instead of reconnecting. A code outside
|
|
83
|
+
* this set is never read as a refusal: `server_is_overloaded` and `slow_down`
|
|
84
|
+
* take the overload arm, `rate_limit_exceeded` takes the rate-limit arm, and
|
|
85
|
+
* everything else falls through to `ApiError::Retryable` and is reconnected up
|
|
86
|
+
* to `stream_max_retries`.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately scoped to content refusals. `insufficient_quota` and
|
|
89
|
+
* `context_length_exceeded` are terminal for Codex too, but they are separate
|
|
90
|
+
* failure families this proxy already classifies through its own quota and
|
|
91
|
+
* context paths, and pulling them in here would change their retry behavior
|
|
92
|
+
* with no evidence asking for it.
|
|
93
|
+
*/
|
|
94
|
+
const TERMINAL_REFUSAL_CODES = new Set<string>([
|
|
95
|
+
CYBER_POLICY_ERROR_CODE,
|
|
96
|
+
"misalignment_policy_violation",
|
|
97
|
+
"invalid_prompt",
|
|
98
|
+
"bio_policy",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
/** True when an upstream error code is a refusal the client must not retry. */
|
|
102
|
+
export function isTerminalRefusalCode(code: string | null | undefined): boolean {
|
|
103
|
+
return typeof code === "string" && TERMINAL_REFUSAL_CODES.has(code);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Stand-in copy for a refusal the upstream sent without a message of its own. */
|
|
107
|
+
export const TERMINAL_REFUSAL_FALLBACK_MESSAGE = "The upstream refused this request.";
|
|
108
|
+
|
|
109
|
+
/** Readable copy for a refusal code, used when the upstream carried no message. */
|
|
110
|
+
export function terminalRefusalFallbackMessage(code: string): string {
|
|
111
|
+
return code === CYBER_POLICY_ERROR_CODE
|
|
112
|
+
? CYBER_POLICY_FALLBACK_MESSAGE
|
|
113
|
+
: TERMINAL_REFUSAL_FALLBACK_MESSAGE;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Name the refusal code behind upstream safety copy when the structured code
|
|
118
|
+
* was not carried on the wire.
|
|
119
|
+
*
|
|
120
|
+
* Same discipline as {@link isCyberPolicyMessage}: whole distinctive phrases
|
|
121
|
+
* only. A loose match here is the mirror-image defect — it would end a turn
|
|
122
|
+
* that a genuine transient failure would have retried successfully — so a
|
|
123
|
+
* message that merely mentions safety or blocking does not qualify. Callers
|
|
124
|
+
* must consult this only when the upstream carried no structured code at all,
|
|
125
|
+
* so that a transport failure quoting a refusal in its diagnostic text cannot
|
|
126
|
+
* be promoted past the verdict the upstream actually gave.
|
|
127
|
+
*
|
|
128
|
+
* Copy provenance: "limited access to this content for safety reasons" is the
|
|
129
|
+
* prefix Codex itself matches (tui/src/chatwidget/turn_runtime.rs:8-15) and
|
|
130
|
+
* pairs with `invalid_prompt` in its parser fixture (sse/responses.rs:1358-1366),
|
|
131
|
+
* which also pairs "flagged for possible biological risk" with `bio_policy`.
|
|
132
|
+
* "blocked by our safety systems" is the copy reported in #5176; pairing it
|
|
133
|
+
* with `invalid_prompt` is an inference from the two verified pairings above,
|
|
134
|
+
* not something the upstream source states.
|
|
135
|
+
*/
|
|
136
|
+
export function safetyRefusalCodeFromMessage(text: string): string | undefined {
|
|
137
|
+
const lower = String(text ?? "").toLowerCase();
|
|
138
|
+
if (lower.includes("flagged for possible biological risk")) return "bio_policy";
|
|
139
|
+
if (
|
|
140
|
+
lower.includes("blocked by our safety systems")
|
|
141
|
+
|| lower.includes("limited access to this content for safety reasons")
|
|
142
|
+
) return "invalid_prompt";
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
75
146
|
function isSubscriptionGateMessage(text: string): boolean {
|
|
76
147
|
return (
|
|
77
148
|
text.includes("requires a subscription") ||
|
|
@@ -185,6 +256,16 @@ export function isClientClosedMessage(text: string): boolean {
|
|
|
185
256
|
);
|
|
186
257
|
}
|
|
187
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Ambiguous-reset refusal wording owned by this proxy (src/lib/upstream-retry.ts):
|
|
261
|
+
* the upstream connection closed before any response arrived, so the request may
|
|
262
|
+
* already have been processed and automatic replay was stopped. Matched narrowly
|
|
263
|
+
* so a provider-sent message is never relabeled by it.
|
|
264
|
+
*/
|
|
265
|
+
export function isUpstreamResetReplayRefusedMessage(text: string): boolean {
|
|
266
|
+
return text.toLowerCase().includes("connection closed before a response was received");
|
|
267
|
+
}
|
|
268
|
+
|
|
188
269
|
export function classifyError(status: number, type: string, message: string): OcxErrorPayload {
|
|
189
270
|
const text = message.toLowerCase();
|
|
190
271
|
if (type === "previous_response_not_found") {
|
|
@@ -376,21 +457,7 @@ export function isRateLimitOrQuotaFailureMessage(message: string): boolean {
|
|
|
376
457
|
return normalized.toLowerCase().includes("usage limit");
|
|
377
458
|
}
|
|
378
459
|
|
|
379
|
-
|
|
380
|
-
export function parseRetryAfterFromMessage(message: string): number | undefined {
|
|
381
|
-
const patterns = [
|
|
382
|
-
/try again in (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
383
|
-
/retry after (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
384
|
-
/retry[- ]after[:\s]+(\d+)/i,
|
|
385
|
-
];
|
|
386
|
-
for (const pattern of patterns) {
|
|
387
|
-
const match = message.match(pattern);
|
|
388
|
-
if (!match?.[1]) continue;
|
|
389
|
-
const seconds = Number.parseFloat(match[1]);
|
|
390
|
-
if (Number.isFinite(seconds) && seconds > 0) return Math.ceil(seconds);
|
|
391
|
-
}
|
|
392
|
-
return undefined;
|
|
393
|
-
}
|
|
460
|
+
export { parseRetryAfterFromMessage };
|
|
394
461
|
|
|
395
462
|
/** Infer HTTP status from adapter terminal error text (provider-agnostic keyword matching). */
|
|
396
463
|
export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response rules both raw outbound transports have to apply themselves.
|
|
3
|
+
*
|
|
4
|
+
* `fetch` applies them below the Response constructor. `src/lib/pinned-http.ts` and
|
|
5
|
+
* `src/lib/socks5-fetch.ts` assemble a Response from a socket instead, so each one answers the
|
|
6
|
+
* same questions on its own — and a rule written twice is a rule that drifts. It already had:
|
|
7
|
+
* the SOCKS helper excluded 204 and the pinned helper excluded nothing, so the same no-content
|
|
8
|
+
* upstream answer behaved differently depending on which transport carried the request.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Statuses the Fetch specification defines as null-body.
|
|
13
|
+
*
|
|
14
|
+
* `new Response(body, { status })` throws a TypeError for a non-null body on any of these, so a
|
|
15
|
+
* transport that attaches its stream unconditionally converts a valid no-content answer into a
|
|
16
|
+
* construction failure. There are no body bytes to wait for either, so a transport that streams
|
|
17
|
+
* one of these holds the caller until the peer closes a connection it is entitled to keep alive.
|
|
18
|
+
*
|
|
19
|
+
* 101 and 103 are null-body statuses too, but neither is a final Response status these helpers
|
|
20
|
+
* support. 103 and every other informational head is consumed while looking for the final one,
|
|
21
|
+
* and 101 hands the connection to a protocol neither helper speaks, which is outside what they
|
|
22
|
+
* construct a Response for at all. The three below are the statuses these transports actually
|
|
23
|
+
* have to answer for.
|
|
24
|
+
*
|
|
25
|
+
* https://fetch.spec.whatwg.org/#null-body-status
|
|
26
|
+
*/
|
|
27
|
+
export function isNullBodyStatus(status: number): boolean {
|
|
28
|
+
return status === 204 || status === 205 || status === 304;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a transport must do with the coding a response declares. */
|
|
32
|
+
export type ContentCoding =
|
|
33
|
+
| { kind: "identity" }
|
|
34
|
+
| { kind: "decodable"; format: "gzip" | "deflate" }
|
|
35
|
+
| { kind: "unsupported"; coding: string };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Classify the `content-encoding` a raw transport received.
|
|
39
|
+
*
|
|
40
|
+
* `fetch` undoes a content-coding below the Response constructor. A transport that assembles a
|
|
41
|
+
* body from a socket hands the coded bytes to whatever reads them instead: `response.json()`
|
|
42
|
+
* throws a SyntaxError on the gzip magic number and an SSE reader sees noise rather than frames.
|
|
43
|
+
* Only `gzip` and `deflate` can be undone with `DecompressionStream`, so anything else is
|
|
44
|
+
* reported as unsupported and each transport refuses it under its own error type rather than
|
|
45
|
+
* surfacing bytes no caller can parse.
|
|
46
|
+
*
|
|
47
|
+
* This reads the coding the response actually carries and nothing else. A caller whose
|
|
48
|
+
* `accept-encoding` names a coding this code cannot undo is not the problem; what the peer
|
|
49
|
+
* chose to send is.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyContentCoding(headers: Headers): ContentCoding {
|
|
52
|
+
const coding = (headers.get("content-encoding") ?? "").trim().toLowerCase();
|
|
53
|
+
if (coding === "" || coding === "identity") return { kind: "identity" };
|
|
54
|
+
if (coding === "gzip" || coding === "x-gzip") return { kind: "decodable", format: "gzip" };
|
|
55
|
+
if (coding === "deflate") return { kind: "decodable", format: "deflate" };
|
|
56
|
+
return { kind: "unsupported", coding };
|
|
57
|
+
}
|