@iislee/opencodex 2.31.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
package/src/images/loop.ts
CHANGED
|
@@ -258,10 +258,11 @@ export interface ImageBridgeDeps {
|
|
|
258
258
|
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
259
259
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
260
260
|
/**
|
|
261
|
-
* Optional 429
|
|
262
|
-
* rotated
|
|
261
|
+
* Optional 429 failover for the routed (non-xAI) model. Return a rebuilt adapter for the
|
|
262
|
+
* rotated credential, or null when the pool is exhausted. Async hooks support OAuth refresh;
|
|
263
|
+
* existing synchronous key-pool hooks remain valid.
|
|
263
264
|
*/
|
|
264
|
-
on429?: (retryAfterHeader: string | null) => ProviderAdapter | null
|
|
265
|
+
on429?: (retryAfterHeader: string | null) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
|
|
265
266
|
/** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
|
|
266
267
|
retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
|
|
267
268
|
/** Called when the bridged Responses stream completes (parity with runTurn / routed paths). */
|
|
@@ -572,7 +573,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
572
573
|
}
|
|
573
574
|
// 429 key-failover parity with web-search / normal routed path.
|
|
574
575
|
while (prepared.response.status === 429 && deps.on429) {
|
|
575
|
-
const rotated = deps.on429(prepared.response.headers.get("retry-after"));
|
|
576
|
+
const rotated = await deps.on429(prepared.response.headers.get("retry-after"));
|
|
576
577
|
if (!rotated) break;
|
|
577
578
|
try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
578
579
|
adapter = rotated;
|
package/src/images/plan.ts
CHANGED
|
@@ -48,11 +48,12 @@ export async function planImageBridge(
|
|
|
48
48
|
if (config.images?.bridgeEnabled !== true) return undefined;
|
|
49
49
|
if (!parsed._imageGeneration) return undefined;
|
|
50
50
|
const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
|
|
51
|
-
const toolNames = new Set(
|
|
52
|
-
|
|
53
|
-
.filter(name => toolAllowed({ name })),
|
|
54
|
-
);
|
|
51
|
+
const toolNames = new Set([...parsed._imageGeneration.toolNames].filter(name => toolAllowed({ name })));
|
|
52
|
+
if (toolAllowed({ name: IMAGE_GEN_TOOL_NAME })) toolNames.add(IMAGE_GEN_TOOL_NAME);
|
|
55
53
|
if (toolNames.size === 0) return undefined;
|
|
54
|
+
// Responses advertises and rewrites authorized aliases to this synthetic name, so the loop
|
|
55
|
+
// must always intercept it once any image-generation name has armed the bridge.
|
|
56
|
+
toolNames.add(IMAGE_GEN_TOOL_NAME);
|
|
56
57
|
// Don't intercept for OpenAI native passthrough
|
|
57
58
|
const host = (() => { try { return new URL(routedProvider.baseUrl).hostname; } catch { return ""; } })();
|
|
58
59
|
if (host === "api.openai.com") return undefined;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-scoped exceptions to strict managed-fragment ownership.
|
|
3
|
+
*
|
|
4
|
+
* Most clients must preserve every byte represented by their managed
|
|
5
|
+
* contribution. A client that persists runtime-derived fields back into an
|
|
6
|
+
* OpenCodex-owned fragment needs a narrower contract: name the exact paths it
|
|
7
|
+
* may rewrite, then fingerprint everything else. Keeping that policy here
|
|
8
|
+
* prevents a client quirk from weakening the shared classifier.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
OPENCODE_PROVIDER_ID,
|
|
12
|
+
type ManagedContribution,
|
|
13
|
+
type ManagedFragment,
|
|
14
|
+
} from "../clients/config-export";
|
|
15
|
+
import { canonicalContribution, fingerprint, semanticContribution } from "./ownership";
|
|
16
|
+
|
|
17
|
+
type JsonObject = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
function isObject(value: unknown): value is JsonObject {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pathStartsWith(path: readonly string[], prefix: readonly string[]): boolean {
|
|
24
|
+
return prefix.length <= path.length && prefix.every((part, index) => path[index] === part);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Delete one nested object member and prune only the empty ancestors created by that deletion. */
|
|
28
|
+
function deletePath(root: unknown, path: readonly string[]): void {
|
|
29
|
+
if (!isObject(root) || path.length === 0) return;
|
|
30
|
+
const parents: Array<{ parent: JsonObject; key: string }> = [];
|
|
31
|
+
let cursor: JsonObject = root;
|
|
32
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
33
|
+
const key = path[index]!;
|
|
34
|
+
const next = cursor[key];
|
|
35
|
+
if (!isObject(next)) return;
|
|
36
|
+
parents.push({ parent: cursor, key });
|
|
37
|
+
cursor = next;
|
|
38
|
+
}
|
|
39
|
+
delete cursor[path[path.length - 1]!];
|
|
40
|
+
for (let index = parents.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
if (Object.keys(cursor).length > 0) break;
|
|
42
|
+
const { parent, key } = parents[index]!;
|
|
43
|
+
delete parent[key];
|
|
44
|
+
cursor = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cloneFragment(fragment: ManagedFragment): ManagedFragment {
|
|
49
|
+
return {
|
|
50
|
+
path: [...fragment.path],
|
|
51
|
+
value: structuredClone(fragment.value),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Paths a client is documented to derive after OpenCodex writes its block.
|
|
57
|
+
*
|
|
58
|
+
* ZCode 3.8.1 persists reasoning and output defaults for every generated
|
|
59
|
+
* model. It may also fill a context default when OpenCodex intentionally
|
|
60
|
+
* omitted one; an authoritative context emitted by OpenCodex remains
|
|
61
|
+
* protected and is never listed here.
|
|
62
|
+
*/
|
|
63
|
+
export function refreshablePathsOf(
|
|
64
|
+
contribution: ManagedContribution,
|
|
65
|
+
): readonly (readonly string[])[] {
|
|
66
|
+
if (contribution.clientId !== "zcode") return [];
|
|
67
|
+
const fragment = contribution.fragments.find(candidate => (
|
|
68
|
+
candidate.path.length === 2
|
|
69
|
+
&& candidate.path[0] === "provider"
|
|
70
|
+
&& candidate.path[1] === OPENCODE_PROVIDER_ID
|
|
71
|
+
));
|
|
72
|
+
if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return [];
|
|
73
|
+
|
|
74
|
+
const paths: string[][] = [];
|
|
75
|
+
for (const modelId of Object.keys(fragment.value.models).sort()) {
|
|
76
|
+
const entry = fragment.value.models[modelId];
|
|
77
|
+
if (!isObject(entry)) continue;
|
|
78
|
+
const base = [...fragment.path, "models", modelId];
|
|
79
|
+
paths.push([...base, "reasoning"]);
|
|
80
|
+
paths.push([...base, "limit", "output"]);
|
|
81
|
+
const limit = entry.limit;
|
|
82
|
+
if (!isObject(limit) || typeof limit.context !== "number") {
|
|
83
|
+
paths.push([...base, "limit", "context"]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return paths;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validRefreshablePaths(
|
|
90
|
+
contribution: ManagedContribution,
|
|
91
|
+
value: unknown,
|
|
92
|
+
): value is readonly (readonly string[])[] {
|
|
93
|
+
if (contribution.clientId !== "zcode" || !Array.isArray(value) || value.length === 0) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const fragment = contribution.fragments.find(candidate => (
|
|
97
|
+
candidate.path.length === 2
|
|
98
|
+
&& candidate.path[0] === "provider"
|
|
99
|
+
&& candidate.path[1] === OPENCODE_PROVIDER_ID
|
|
100
|
+
));
|
|
101
|
+
if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return false;
|
|
102
|
+
|
|
103
|
+
const modelIds = new Set(Object.keys(fragment.value.models));
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
return value.every(path => {
|
|
106
|
+
if (!Array.isArray(path) || !path.every(part => typeof part === "string")) return false;
|
|
107
|
+
const modelId = path[3];
|
|
108
|
+
const isReasoning = path.length === 5 && path[4] === "reasoning";
|
|
109
|
+
const isLimitDefault = path.length === 6
|
|
110
|
+
&& path[4] === "limit"
|
|
111
|
+
&& (path[5] === "output" || path[5] === "context");
|
|
112
|
+
const key = path.join("\u0000");
|
|
113
|
+
if (
|
|
114
|
+
path[0] !== "provider"
|
|
115
|
+
|| path[1] !== OPENCODE_PROVIDER_ID
|
|
116
|
+
|| path[2] !== "models"
|
|
117
|
+
|| typeof modelId !== "string"
|
|
118
|
+
|| !modelIds.has(modelId)
|
|
119
|
+
|| (!isReasoning && !isLimitDefault)
|
|
120
|
+
|| seen.has(key)
|
|
121
|
+
) return false;
|
|
122
|
+
seen.add(key);
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function contributionWithoutRefreshablePaths(
|
|
128
|
+
contribution: ManagedContribution,
|
|
129
|
+
refreshablePaths: readonly (readonly string[])[],
|
|
130
|
+
): ManagedContribution {
|
|
131
|
+
const fragments = contribution.fragments.map(cloneFragment);
|
|
132
|
+
for (const refreshablePath of refreshablePaths) {
|
|
133
|
+
for (const fragment of fragments) {
|
|
134
|
+
if (!pathStartsWith(refreshablePath, fragment.path)) continue;
|
|
135
|
+
deletePath(fragment.value, refreshablePath.slice(fragment.path.length));
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { ...contribution, fragments };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Fingerprint a contribution after removing only its explicitly refreshable paths. */
|
|
143
|
+
export function protectedContributionFingerprint(
|
|
144
|
+
contribution: ManagedContribution,
|
|
145
|
+
refreshablePaths: readonly (readonly string[])[],
|
|
146
|
+
): string {
|
|
147
|
+
return fingerprint(canonicalContribution(
|
|
148
|
+
contributionWithoutRefreshablePaths(contribution, refreshablePaths),
|
|
149
|
+
));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Semantic protected fingerprint that ignores JSON object-key order only. */
|
|
153
|
+
export function semanticProtectedContributionFingerprint(
|
|
154
|
+
contribution: ManagedContribution,
|
|
155
|
+
refreshablePaths: readonly (readonly string[])[],
|
|
156
|
+
): string {
|
|
157
|
+
return fingerprint(semanticContribution(
|
|
158
|
+
contributionWithoutRefreshablePaths(contribution, refreshablePaths),
|
|
159
|
+
));
|
|
160
|
+
}
|
|
@@ -22,8 +22,38 @@ export function fingerprint(text: string): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Canonicalize JSON object members recursively for semantic comparisons.
|
|
26
|
+
* Arrays stay ordered because their position can carry configuration meaning.
|
|
27
|
+
*/
|
|
28
|
+
function semanticJsonValue(value: unknown): unknown {
|
|
29
|
+
if (Array.isArray(value)) return value.map(semanticJsonValue);
|
|
30
|
+
if (value === null || typeof value !== "object") return value;
|
|
31
|
+
|
|
32
|
+
const record = value as Record<string, unknown>;
|
|
33
|
+
return Object.fromEntries(
|
|
34
|
+
Object.keys(record)
|
|
35
|
+
.sort()
|
|
36
|
+
.map(key => [key, semanticJsonValue(record[key])]),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stable semantic bytes of a contribution. Third-party clients may
|
|
42
|
+
* re-serialize JSON object members in a different order; that formatting-only
|
|
43
|
+
* rewrite must not look like a protected-value edit.
|
|
44
|
+
*/
|
|
45
|
+
export function semanticContribution(contribution: ManagedContribution): string {
|
|
46
|
+
const sorted = [...contribution.fragments].sort((a, b) => {
|
|
47
|
+
const left = a.path.join("\u0000");
|
|
48
|
+
const right = b.path.join("\u0000");
|
|
49
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
50
|
+
});
|
|
51
|
+
return JSON.stringify(sorted.map(fragment => [fragment.path, semanticJsonValue(fragment.value)]));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Legacy-compatible bytes used by existing persisted fingerprints. Fragment
|
|
56
|
+
* paths are stable, while nested object insertion order remains exact.
|
|
27
57
|
*/
|
|
28
58
|
export function canonicalContribution(contribution: ManagedContribution): string {
|
|
29
59
|
const sorted = [...contribution.fragments].sort((a, b) => {
|
|
@@ -41,6 +71,20 @@ export interface OwnershipRecord {
|
|
|
41
71
|
fileFingerprint: string;
|
|
42
72
|
/** Hash of our contribution — detects catalog/port drift. */
|
|
43
73
|
blockFingerprint: string;
|
|
74
|
+
/** Key-order-independent companion for JSON clients that normalize objects. */
|
|
75
|
+
semanticBlockFingerprint?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Hash of the fields the client must not rewrite. Present only when a
|
|
78
|
+
* client has explicitly declared runtime-derived paths below.
|
|
79
|
+
*/
|
|
80
|
+
protectedBlockFingerprint?: string;
|
|
81
|
+
/** Key-order-independent companion to `protectedBlockFingerprint`. */
|
|
82
|
+
semanticProtectedBlockFingerprint?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Exact document paths a client may derive after apply. These are recorded
|
|
85
|
+
* per operation so later catalog changes cannot widen an older grant.
|
|
86
|
+
*/
|
|
87
|
+
refreshablePaths?: readonly (readonly string[])[];
|
|
44
88
|
/** The exact paths we own. Removal touches these and nothing else. */
|
|
45
89
|
fragmentPaths: readonly (readonly string[])[];
|
|
46
90
|
/**
|
|
@@ -12,6 +12,8 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import {
|
|
14
14
|
EXPORT_CLIENTS,
|
|
15
|
+
clineConfigPath,
|
|
16
|
+
clineHomeDir,
|
|
15
17
|
dshConfigPath,
|
|
16
18
|
dshHomeDir,
|
|
17
19
|
gajaeConfigPath,
|
|
@@ -149,6 +151,11 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
149
151
|
// is set. Same choice as OMP, whose detect signal is `ompAgentDir`.
|
|
150
152
|
detectDir: (env = process.env, home = homedir()) => primeAgentDir(env, home),
|
|
151
153
|
},
|
|
154
|
+
cline: {
|
|
155
|
+
id: "cline",
|
|
156
|
+
configPath: (env = process.env, home = homedir()) => clineConfigPath(env, home),
|
|
157
|
+
detectDir: (env = process.env, home = homedir()) => clineHomeDir(env, home),
|
|
158
|
+
},
|
|
152
159
|
};
|
|
153
160
|
|
|
154
161
|
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
@@ -12,7 +12,13 @@ import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel
|
|
|
12
12
|
import type { OcxConfig } from "../types";
|
|
13
13
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
14
14
|
import { SNAPSHOT_RETENTION } from "./journal";
|
|
15
|
-
import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
|
|
15
|
+
import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
|
|
16
|
+
import {
|
|
17
|
+
protectedContributionFingerprint,
|
|
18
|
+
refreshablePathsOf,
|
|
19
|
+
semanticProtectedContributionFingerprint,
|
|
20
|
+
validRefreshablePaths,
|
|
21
|
+
} from "./ownership-policy";
|
|
16
22
|
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
17
23
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
18
24
|
|
|
@@ -106,10 +112,10 @@ export function blockedContainerPath(
|
|
|
106
112
|
* values lets another integration or a user add a sibling without blocking a
|
|
107
113
|
* later refresh, while a change inside our block still fails closed.
|
|
108
114
|
*/
|
|
109
|
-
function
|
|
115
|
+
function recordedContribution(
|
|
110
116
|
doc: unknown,
|
|
111
117
|
record: OwnershipRecord,
|
|
112
|
-
):
|
|
118
|
+
): ManagedContribution | null {
|
|
113
119
|
if (
|
|
114
120
|
!Array.isArray(record.fragmentPaths)
|
|
115
121
|
|| record.fragmentPaths.length === 0
|
|
@@ -125,10 +131,72 @@ function recordedFragmentFingerprint(
|
|
|
125
131
|
if (value === undefined) return null;
|
|
126
132
|
fragments.push({ path, value });
|
|
127
133
|
}
|
|
128
|
-
return
|
|
134
|
+
return {
|
|
129
135
|
clientId: record.clientId,
|
|
130
136
|
fragments,
|
|
131
|
-
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Prove that every protected field still matches what OpenCodex wrote.
|
|
142
|
+
*
|
|
143
|
+
* New records carry an operation-scoped protected fingerprint and the exact
|
|
144
|
+
* paths excluded from it. Legacy records can recover only when the desired
|
|
145
|
+
* contribution has not moved since apply; otherwise catalog drift and a
|
|
146
|
+
* foreign edit are indistinguishable, so the classifier keeps failing closed.
|
|
147
|
+
*/
|
|
148
|
+
function recordedBlockIsOwned(
|
|
149
|
+
doc: unknown,
|
|
150
|
+
record: OwnershipRecord,
|
|
151
|
+
desired: ManagedContribution,
|
|
152
|
+
): boolean {
|
|
153
|
+
const observed = recordedContribution(doc, record);
|
|
154
|
+
if (!observed) return false;
|
|
155
|
+
if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
|
|
156
|
+
|
|
157
|
+
const observedSemanticFingerprint = fingerprint(semanticContribution(observed));
|
|
158
|
+
if (
|
|
159
|
+
typeof record.semanticBlockFingerprint === "string"
|
|
160
|
+
&& observedSemanticFingerprint === record.semanticBlockFingerprint
|
|
161
|
+
) return true;
|
|
162
|
+
|
|
163
|
+
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
164
|
+
if (
|
|
165
|
+
desiredFingerprint === record.blockFingerprint
|
|
166
|
+
&& observedSemanticFingerprint === fingerprint(semanticContribution(desired))
|
|
167
|
+
) return true;
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
typeof record.protectedBlockFingerprint === "string"
|
|
171
|
+
&& validRefreshablePaths(observed, record.refreshablePaths)
|
|
172
|
+
&& record.refreshablePaths.length > 0
|
|
173
|
+
) {
|
|
174
|
+
const observedProtectedFingerprint = protectedContributionFingerprint(
|
|
175
|
+
observed,
|
|
176
|
+
record.refreshablePaths,
|
|
177
|
+
);
|
|
178
|
+
if (observedProtectedFingerprint === record.protectedBlockFingerprint) return true;
|
|
179
|
+
|
|
180
|
+
const observedSemanticProtectedFingerprint = semanticProtectedContributionFingerprint(
|
|
181
|
+
observed,
|
|
182
|
+
record.refreshablePaths,
|
|
183
|
+
);
|
|
184
|
+
if (
|
|
185
|
+
typeof record.semanticProtectedBlockFingerprint === "string"
|
|
186
|
+
&& observedSemanticProtectedFingerprint === record.semanticProtectedBlockFingerprint
|
|
187
|
+
) return true;
|
|
188
|
+
|
|
189
|
+
return protectedContributionFingerprint(desired, record.refreshablePaths)
|
|
190
|
+
=== record.protectedBlockFingerprint
|
|
191
|
+
&& observedSemanticProtectedFingerprint
|
|
192
|
+
=== semanticProtectedContributionFingerprint(desired, record.refreshablePaths);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (desiredFingerprint !== record.blockFingerprint) return false;
|
|
196
|
+
const legacyPaths = refreshablePathsOf(desired);
|
|
197
|
+
return legacyPaths.length > 0
|
|
198
|
+
&& semanticProtectedContributionFingerprint(observed, legacyPaths)
|
|
199
|
+
=== semanticProtectedContributionFingerprint(desired, legacyPaths);
|
|
132
200
|
}
|
|
133
201
|
|
|
134
202
|
/**
|
|
@@ -191,7 +259,7 @@ export function classifyIntegration(input: {
|
|
|
191
259
|
* conflict no matter what the rest of the file looks like, so the sibling-
|
|
192
260
|
* edit exemption below can never mask it.
|
|
193
261
|
*/
|
|
194
|
-
if (
|
|
262
|
+
if (!recordedBlockIsOwned(input.parsed, input.record, input.contribution)) {
|
|
195
263
|
return { state: "conflict", reason: "foreign-edit" };
|
|
196
264
|
}
|
|
197
265
|
if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
|
|
@@ -218,7 +286,11 @@ export function classifyIntegration(input: {
|
|
|
218
286
|
}
|
|
219
287
|
return { state: "stale" };
|
|
220
288
|
}
|
|
221
|
-
|
|
289
|
+
const desiredFingerprint = typeof input.record.semanticBlockFingerprint === "string"
|
|
290
|
+
? fingerprint(semanticContribution(input.contribution))
|
|
291
|
+
: fingerprint(canonicalContribution(input.contribution));
|
|
292
|
+
const recordedFingerprint = input.record.semanticBlockFingerprint ?? input.record.blockFingerprint;
|
|
293
|
+
return recordedFingerprint === desiredFingerprint
|
|
222
294
|
? { state: "current" }
|
|
223
295
|
: { state: "stale" };
|
|
224
296
|
}
|
|
@@ -15,7 +15,18 @@ import { EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../c
|
|
|
15
15
|
import { isLoopbackHostname } from "../codex/inject";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
fingerprint,
|
|
20
|
+
canonicalContribution,
|
|
21
|
+
fragmentPathsOf,
|
|
22
|
+
semanticContribution,
|
|
23
|
+
type OwnershipRecord,
|
|
24
|
+
} from "./ownership";
|
|
25
|
+
import {
|
|
26
|
+
protectedContributionFingerprint,
|
|
27
|
+
refreshablePathsOf,
|
|
28
|
+
semanticProtectedContributionFingerprint,
|
|
29
|
+
} from "./ownership-policy";
|
|
19
30
|
import { createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
20
31
|
import { INTEGRATION_CLIENTS, isLoopbackOnly, type IntegrationClientId } from "./registry";
|
|
21
32
|
import { classifyIntegration, exportContextOf } from "./state";
|
|
@@ -353,12 +364,22 @@ function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: bo
|
|
|
353
364
|
opId, clientId, kind: classified.state === "stale" ? "refresh" : "apply", at, configPath,
|
|
354
365
|
snapshot, resultFingerprint: fingerprint(text), resultAbsent: false, priorRecord: record,
|
|
355
366
|
};
|
|
367
|
+
const refreshablePaths = refreshablePathsOf(contribution);
|
|
356
368
|
return commit({
|
|
357
369
|
io, store, clientId, configPath, before, nextText: text, state: "current",
|
|
358
370
|
priorRecord: record,
|
|
359
371
|
record: {
|
|
360
372
|
clientId, configPath, fileFingerprint: fingerprint(text),
|
|
361
373
|
blockFingerprint: fingerprint(canonicalContribution(contribution)),
|
|
374
|
+
semanticBlockFingerprint: fingerprint(semanticContribution(contribution)),
|
|
375
|
+
...(refreshablePaths.length > 0 ? {
|
|
376
|
+
protectedBlockFingerprint: protectedContributionFingerprint(contribution, refreshablePaths),
|
|
377
|
+
semanticProtectedBlockFingerprint: semanticProtectedContributionFingerprint(
|
|
378
|
+
contribution,
|
|
379
|
+
refreshablePaths,
|
|
380
|
+
),
|
|
381
|
+
refreshablePaths,
|
|
382
|
+
} : {}),
|
|
362
383
|
fragmentPaths: fragmentPathsOf(contribution), createdContainers: created,
|
|
363
384
|
appliedAt: at, opId,
|
|
364
385
|
},
|
|
@@ -550,7 +571,10 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
550
571
|
? (restoredText === null ? "absent" : "conflict")
|
|
551
572
|
: !recordDescribesBytes
|
|
552
573
|
? "conflict"
|
|
553
|
-
:
|
|
574
|
+
: (
|
|
575
|
+
restoredRecord.semanticBlockFingerprint === fingerprint(semanticContribution(fresh))
|
|
576
|
+
|| restoredRecord.blockFingerprint === fingerprint(canonicalContribution(fresh))
|
|
577
|
+
)
|
|
554
578
|
? "current"
|
|
555
579
|
: "stale";
|
|
556
580
|
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
"assertions": [
|
|
275
275
|
{ "id": "text", "operator": "normalized_text_equals", "selector": "/client/response/normalizedText", "expected": "OK", "required": true },
|
|
276
276
|
{ "id": "terminal", "operator": "terminal_signal_equals", "selector": "/client/response/terminal", "expected": "completed", "required": true },
|
|
277
|
-
{ "id": "phase", "operator": "json_path_equals", "selector": "/client/response/events/
|
|
277
|
+
{ "id": "phase", "operator": "json_path_equals", "selector": "/client/response/events/7/data/item/phase", "expected": "final_answer", "required": true }
|
|
278
278
|
]
|
|
279
279
|
},
|
|
280
280
|
{
|
package/src/lib/admin-secrets.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { lstatSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { getConfigDir } from "../config";
|
|
@@ -23,3 +24,26 @@ export function loadAdminTokenFromFile(configDir = getConfigDir()): string | nul
|
|
|
23
24
|
export function configuredAdminToken(configDir = getConfigDir(), env: NodeJS.ProcessEnv = process.env): string | null {
|
|
24
25
|
return env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim() || loadAdminTokenFromFile(configDir);
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
export const ADMIN_TOKEN_PREFIX = "ocx_admin_";
|
|
29
|
+
|
|
30
|
+
function secretTextEquals(left: string, right: string): boolean {
|
|
31
|
+
const a = Buffer.from(left);
|
|
32
|
+
const b = Buffer.from(right);
|
|
33
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when `token` is a management credential: minted `ocx_admin_…` shape, or
|
|
38
|
+
* byte-equal to the configured admin token (env or admin-api-token file).
|
|
39
|
+
* Used by the service write/start chokepoint and by doctor so the two cannot drift.
|
|
40
|
+
*/
|
|
41
|
+
export function tokenCollidesWithAdmin(
|
|
42
|
+
token: string,
|
|
43
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
44
|
+
configDir = getConfigDir(),
|
|
45
|
+
): boolean {
|
|
46
|
+
if (token.startsWith(ADMIN_TOKEN_PREFIX)) return true;
|
|
47
|
+
const admin = configuredAdminToken(configDir, env);
|
|
48
|
+
return admin !== null && secretTextEquals(token, admin);
|
|
49
|
+
}
|
package/src/lib/bounded-body.ts
CHANGED
|
@@ -103,6 +103,16 @@ function cancelWithoutWaiting(reader: ReadableStreamDefaultReader<Uint8Array>, r
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function cancelBodyWithoutWaiting(body: ReadableStream<Uint8Array>, reason?: unknown): void {
|
|
107
|
+
// A signal can already be aborted before a reader is attached. Still settle the
|
|
108
|
+
// original body so fetch-backed streams cannot retain a rejected read in that gap.
|
|
109
|
+
try {
|
|
110
|
+
void body.cancel(reason).catch(() => undefined);
|
|
111
|
+
} catch {
|
|
112
|
+
// A locked or non-conforming stream may throw synchronously from cancel().
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
/**
|
|
107
117
|
* Consume the original response body as raw bytes under a strict memory ceiling.
|
|
108
118
|
*
|
|
@@ -208,9 +218,11 @@ export async function readBoundedResponseBody(
|
|
|
208
218
|
options: BoundedBodyOptions = {},
|
|
209
219
|
): Promise<BoundedBodyResult> {
|
|
210
220
|
const signal = options.signal;
|
|
211
|
-
if (signal?.aborted) throw signal.reason;
|
|
212
|
-
|
|
213
221
|
const body = response.body;
|
|
222
|
+
if (signal?.aborted) {
|
|
223
|
+
if (body) cancelBodyWithoutWaiting(body, signal.reason);
|
|
224
|
+
throw signal.reason;
|
|
225
|
+
}
|
|
214
226
|
if (!body) {
|
|
215
227
|
return {
|
|
216
228
|
text: "",
|
|
@@ -40,6 +40,7 @@ const INITIAL_OWNED_PATHS = [
|
|
|
40
40
|
"artifacts",
|
|
41
41
|
"auth.json",
|
|
42
42
|
"auth.store.lock",
|
|
43
|
+
"admin-api-token",
|
|
43
44
|
"catalog-backup.json",
|
|
44
45
|
"claude-env.sh",
|
|
45
46
|
"codex-accounts.json",
|
|
@@ -333,6 +334,25 @@ export function removeOwnedConfigState(configDir: string): ConfigRemovalResult {
|
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
// Per-catalog backups are named `catalog-backup-<16 hex>.json` (catalogBackupPathFor), one per
|
|
338
|
+
// CODEX_HOME, so they cannot be enumerated as literal manifest entries the way every other
|
|
339
|
+
// owned file can. Without this, `ocx uninstall` always reported "unowned files remain" and
|
|
340
|
+
// refused to remove a home OpenCodex created itself — the file is unambiguously ours, produced
|
|
341
|
+
// by our own writer, and the strict hex shape keeps the match from widening.
|
|
342
|
+
for (const name of readdirSync(configDir)) {
|
|
343
|
+
if (!/^catalog-backup-[0-9a-f]{16}\.json$/.test(name)) continue;
|
|
344
|
+
const path = join(configDir, name);
|
|
345
|
+
try {
|
|
346
|
+
removeOwnedEntry(rootPath, path);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return {
|
|
349
|
+
status: "partial",
|
|
350
|
+
reason: `could not remove owned path ${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
351
|
+
residualPaths: [path],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
336
356
|
try {
|
|
337
357
|
unlinkSync(join(configDir, CONFIG_UNINSTALL_MANIFEST));
|
|
338
358
|
unlinkSync(join(configDir, CONFIG_OWNER_FILE));
|
package/src/lib/errors.ts
CHANGED
|
@@ -6,11 +6,18 @@ export interface OcxErrorPayload {
|
|
|
6
6
|
|
|
7
7
|
/** OpenAI / Codex hard block for high-risk cybersecurity activity (HTTP 400 or mid-stream). */
|
|
8
8
|
export const CYBER_POLICY_ERROR_CODE = "cyber_policy";
|
|
9
|
+
export const CYBER_POLICY_FALLBACK_MESSAGE = "Request blocked by the upstream cybersecurity policy.";
|
|
9
10
|
|
|
10
11
|
export function isCyberPolicyCode(code: string | null | undefined): boolean {
|
|
11
12
|
return code === CYBER_POLICY_ERROR_CODE;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Preserve a structured upstream error type; otherwise use the dedicated policy identity. */
|
|
16
|
+
export function cyberPolicyErrorType(type: string | null | undefined): string {
|
|
17
|
+
const trimmed = typeof type === "string" ? type.trim() : "";
|
|
18
|
+
return trimmed || CYBER_POLICY_ERROR_CODE;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* Detect OpenAI cyber-policy refusals from message text when structured `code` was stripped.
|
|
16
23
|
* Matches Codex fallback copy and Cursor/API agent wording (session evidence 2026-07-24).
|
|
@@ -139,9 +146,11 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
139
146
|
return { message, type: "invalid_request_error", code: "client_closed_request" };
|
|
140
147
|
}
|
|
141
148
|
// Codex only shows the dedicated cyber UI when error.code === "cyber_policy".
|
|
142
|
-
//
|
|
149
|
+
// The public wire does not establish invalid_request_error as the canonical type, so
|
|
150
|
+
// message-only classification keeps the dedicated identity instead of inventing one.
|
|
151
|
+
// Structured callers re-apply their real upstream type with cyberPolicyErrorType().
|
|
143
152
|
if (type === CYBER_POLICY_ERROR_CODE || isCyberPolicyMessage(text)) {
|
|
144
|
-
return { message, type:
|
|
153
|
+
return { message, type: CYBER_POLICY_ERROR_CODE, code: CYBER_POLICY_ERROR_CODE };
|
|
145
154
|
}
|
|
146
155
|
// A LOCAL preflight refusal keeps its own code (#1524). The message necessarily says
|
|
147
156
|
// "context window" -- that is what it is refusing on -- so the generic remap below would
|
|
@@ -326,6 +335,12 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
326
335
|
// subscription/permission wording.
|
|
327
336
|
if (isAuthenticationMessage(lower)) return 401;
|
|
328
337
|
if (isSubscriptionGateMessage(lower) || isPermissionMessage(lower)) return 403;
|
|
338
|
+
// Same precedence rule as classifyCursorError: an explicit gRPC FAILED_PRECONDITION is a
|
|
339
|
+
// structured, deterministic rejection, so it outranks the overload keywords that routinely
|
|
340
|
+
// appear beside it ("failed_precondition: model unavailable for this plan"). Without this,
|
|
341
|
+
// the message matched "unavailable" and returned a retryable 503, so clients kept retrying
|
|
342
|
+
// a rejection that can never succeed.
|
|
343
|
+
if (lower.includes("failed_precondition") || lower.includes("failed precondition")) return 400;
|
|
329
344
|
if (
|
|
330
345
|
lower.includes("unavailable") ||
|
|
331
346
|
lower.includes("overloaded") ||
|
|
@@ -401,6 +416,24 @@ export function httpStatusFromTerminalError(error: {
|
|
|
401
416
|
if (message && isClientClosedMessage(message)) return 499;
|
|
402
417
|
if (error.type === "invalid_request_error") return 400;
|
|
403
418
|
if (error.type === "proxy_error") return 500;
|
|
404
|
-
|
|
419
|
+
// A structured server class must not be downgraded to a CLIENT error by message wording.
|
|
420
|
+
// classifyError assigns `server_error` + `upstream_server_error` to every 5xx it sees, so
|
|
421
|
+
// the class is authoritative about blame: the upstream failed, the caller did not send a
|
|
422
|
+
// bad request. What it is NOT authoritative about is which server status fits — a stall is
|
|
423
|
+
// genuinely 504 and an overload genuinely 503, and flattening those to 502 discards
|
|
424
|
+
// information both the log surface and the retry policy read. So message inference still
|
|
425
|
+
// chooses the specific status, and only a client-error verdict is overridden.
|
|
426
|
+
//
|
|
427
|
+
// The override is deliberately narrowed to 400 alone. 429, 499, 401 and 403 are all
|
|
428
|
+
// actionable signals the caller routes on — retry-after, client cancellation, re-auth,
|
|
429
|
+
// entitlement — and overriding them would trade one kind of misreport for another. 400 is
|
|
430
|
+
// the single verdict that both blames the caller and stops the retry, which is the failure
|
|
431
|
+
// being fixed: an upstream 500 whose text happens to contain "malformed" or "invalid
|
|
432
|
+
// request" used to return 400, so Claude Code stopped retrying a retryable failure.
|
|
433
|
+
const structuredServerClass = error.type === "server_error" || error.code === "upstream_server_error";
|
|
434
|
+
if (message) {
|
|
435
|
+
const inferred = inferHttpStatusFromAdapterMessage(message);
|
|
436
|
+
return structuredServerClass && inferred === 400 ? 502 : inferred;
|
|
437
|
+
}
|
|
405
438
|
return 502;
|
|
406
439
|
}
|