@bitkyc08/opencodex 2.84.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, type BigIntStats } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
|
|
@@ -44,9 +44,49 @@ export interface Desktop3pMetadata {
|
|
|
44
44
|
|
|
45
45
|
export const DESKTOP_PICKER_ENTRY_NAME = "opencodex-picker";
|
|
46
46
|
|
|
47
|
+
/** A path that no longer names the observed file was swapped; a same-path failure keeps its own errno. */
|
|
48
|
+
function pathStillNamesExpectedFile(path: string, expected: BigIntStats): boolean {
|
|
49
|
+
try {
|
|
50
|
+
const now = lstatSync(path, { bigint: true });
|
|
51
|
+
return now.isFile() && now.dev === expected.dev && now.ino === expected.ino
|
|
52
|
+
&& now.ctimeNs === expected.ctimeNs;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Read regular config files only; a path swap must not turn an observation into a FIFO wait. */
|
|
57
|
+
export function readDesktopLibraryBytes(path: string): Buffer {
|
|
58
|
+
const expected = lstatSync(path, { bigint: true });
|
|
59
|
+
if (!expected.isFile()) throw new Error("desktop_config_not_regular");
|
|
60
|
+
// POSIX open of a FIFO can itself block, before fstat is possible. Windows has no POSIX
|
|
61
|
+
// FIFO entry; retain descriptor identity validation there without unsupported open flags.
|
|
62
|
+
const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
63
|
+
let fd: number;
|
|
64
|
+
try {
|
|
65
|
+
fd = openSync(path, flags);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// A swap can fail at open — ELOOP on a symlink, ENXIO on a socket — before fstat can
|
|
68
|
+
// classify it. Re-observing the path reports every swap as the documented refusal.
|
|
69
|
+
if (pathStillNamesExpectedFile(path, expected)) throw error;
|
|
70
|
+
throw new Error("desktop_config_not_regular");
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
74
|
+
if (!opened.isFile() || opened.dev !== expected.dev || opened.ino !== expected.ino
|
|
75
|
+
|| opened.ctimeNs !== expected.ctimeNs) throw new Error("desktop_config_not_regular");
|
|
76
|
+
return readFileSync(fd);
|
|
77
|
+
} finally {
|
|
78
|
+
closeSync(fd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Text decoding stays identical to node:fs readFileSync(path, "utf8"). */
|
|
83
|
+
export function readDesktopLibraryText(path: string): string {
|
|
84
|
+
return readDesktopLibraryBytes(path).toString("utf8");
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
export function parseMetadata(path: string): Desktop3pMetadata {
|
|
48
88
|
if (!existsSync(path)) return { entries: [] };
|
|
49
|
-
const parsed = JSON.parse(
|
|
89
|
+
const parsed = JSON.parse(readDesktopLibraryText(path)) as Partial<Desktop3pMetadata>;
|
|
50
90
|
if (!Array.isArray(parsed.entries)) throw new Error("Claude Desktop 3P _meta.json has no entries array");
|
|
51
91
|
return { ...parsed, entries: parsed.entries };
|
|
52
92
|
}
|
|
@@ -82,7 +122,7 @@ export const OPENCODEX_DESKTOP_PROFILE_KEYS = new Set([
|
|
|
82
122
|
|
|
83
123
|
export function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
|
|
84
124
|
if (!existsSync(path)) return {};
|
|
85
|
-
const parsed = JSON.parse(
|
|
125
|
+
const parsed = JSON.parse(readDesktopLibraryText(path)) as unknown;
|
|
86
126
|
if (!isRecord(parsed)) throw new Error("Claude Desktop 3P profile is not a JSON object");
|
|
87
127
|
return Object.fromEntries(
|
|
88
128
|
Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -5,12 +5,12 @@ import { applyRemoteDesktopStore, inspectRemoteDesktopCleanup, restoreRemoteDesk
|
|
|
5
5
|
import { canonicalDirectory } from "./desktop-remote-store-io";
|
|
6
6
|
import {
|
|
7
7
|
resolveDesktop3pConfigLibraryPath, parseMetadata, SAFE_DESKTOP_PROFILE_ID, isRecord,
|
|
8
|
-
isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileForeignKeys,
|
|
8
|
+
isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileForeignKeys, readDesktopLibraryText, readDesktopLibraryBytes,
|
|
9
9
|
type Desktop3pConfigLibraryOptions, type Desktop3pMetadata, type Desktop3pMetadataEntry,
|
|
10
10
|
} from "./desktop-3p-library";
|
|
11
11
|
export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
-
import { copyFileSync, existsSync, mkdirSync,
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, lstatSync, unlinkSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
|
|
16
16
|
import { atomicWriteFileNoFollow } from "../config/atomic-write";
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
reconcileDesktopProfile,
|
|
21
21
|
renderDesktopProfile,
|
|
22
22
|
validDateAlias,
|
|
23
|
+
legacyDesktopProfileWireAlias,
|
|
23
24
|
type DesktopProfileModel,
|
|
24
25
|
} from "./desktop-profile";
|
|
25
26
|
import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../codex/catalog";
|
|
@@ -223,7 +224,7 @@ function collectDesktop3pModels(
|
|
|
223
224
|
...(model.supports1m ? { supports1m: true, prefer1m: true } : {}),
|
|
224
225
|
});
|
|
225
226
|
}
|
|
226
|
-
//
|
|
227
|
+
// Previous profile IDs and both hash generations are compatibility-only and can
|
|
227
228
|
// collide. Bind them in stable route order so changing a family default or
|
|
228
229
|
// rendered ordering can never silently rebind an old Desktop id.
|
|
229
230
|
for (const model of [...rendered].sort((a, b) => a.route.localeCompare(b.route))) {
|
|
@@ -231,7 +232,8 @@ function collectDesktop3pModels(
|
|
|
231
232
|
const providerEnd = model.route.indexOf("/");
|
|
232
233
|
const provider = model.route.slice(0, providerEnd);
|
|
233
234
|
const id = model.route.slice(providerEnd + 1);
|
|
234
|
-
|
|
235
|
+
const storedAlias = reconciled.assignments[model.route]!.alias;
|
|
236
|
+
for (const legacy of [legacyDesktopProfileWireAlias(storedAlias), desktop3pAlias(provider, id), legacyDesktop3pAlias(provider, id)]) {
|
|
235
237
|
if (realAnthropicIds.has(legacy)) {
|
|
236
238
|
console.warn(`[opencodex] Claude Desktop legacy alias ${legacy} for ${model.route} conflicts with a real Anthropic model; ignoring the alias`);
|
|
237
239
|
continue;
|
|
@@ -360,6 +362,11 @@ export function isKnownDesktop3pModelId(id: string): boolean {
|
|
|
360
362
|
return desktop3pRegistry.has(id) || desktop3pRealAnthropicIds.has(id);
|
|
361
363
|
}
|
|
362
364
|
|
|
365
|
+
/** Real catalog IDs retain identity even when their spelling resembles an alias. */
|
|
366
|
+
export function isRealDesktop3pModelId(id: string): boolean {
|
|
367
|
+
return desktop3pRealAnthropicIds.has(id);
|
|
368
|
+
}
|
|
369
|
+
|
|
363
370
|
/** Only missing IDs in the emitted Desktop namespaces are managed-alias errors. */
|
|
364
371
|
export function isUnresolvedDesktop3pAlias(id: string): boolean {
|
|
365
372
|
if (isKnownDesktop3pModelId(id)) return false;
|
|
@@ -369,7 +376,7 @@ export function isUnresolvedDesktop3pAlias(id: string): boolean {
|
|
|
369
376
|
if (isKnownDesktop3pModelId(base)) return false;
|
|
370
377
|
return validDateAlias(base)
|
|
371
378
|
|| /^claude-opus-4-(?:8-)?[a-z][a-z0-9]{2}$/.test(base)
|
|
372
|
-
|| /^claude-opus-4-8-p[0-9a-z]{3}$/.test(base);
|
|
379
|
+
|| /^claude-(?:opus-4-8|fable-5)-p[0-9a-z]{3}$/.test(base);
|
|
373
380
|
}
|
|
374
381
|
|
|
375
382
|
/** Alias selected by the installed profile registry, falling back to the legacy hash shape. */
|
|
@@ -377,6 +384,13 @@ export function activeDesktop3pAlias(provider: string, modelId: string): string
|
|
|
377
384
|
return desktop3pAliasesByRoute.get(`${provider}/${modelId}`) ?? desktop3pAlias(provider, modelId);
|
|
378
385
|
}
|
|
379
386
|
|
|
387
|
+
/** Discovery cannot offer a skipped alias or one owned by a different route. */
|
|
388
|
+
export function resolvableDesktop3pAlias(provider: string, modelId: string): string | undefined {
|
|
389
|
+
if (provider === "anthropic" && modelId.startsWith("claude-")) return modelId;
|
|
390
|
+
const alias = activeDesktop3pAlias(provider, modelId);
|
|
391
|
+
return resolveDesktop3pAlias(alias) === `${provider}/${modelId}` ? alias : undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
380
394
|
/**
|
|
381
395
|
* Generate the complete Claude Desktop 3P gateway config.
|
|
382
396
|
*
|
|
@@ -476,7 +490,7 @@ export function inspectDesktop3pConfigLibrary(
|
|
|
476
490
|
let profile: Record<string, unknown>;
|
|
477
491
|
let fingerprint: string;
|
|
478
492
|
try {
|
|
479
|
-
const source =
|
|
493
|
+
const source = readDesktopLibraryText(selectedProfilePath);
|
|
480
494
|
const parsed = JSON.parse(source) as unknown;
|
|
481
495
|
if (!isRecord(parsed)) return { kind: "broken", libraryPath, selectedProfilePath, appliedId, residualPaths, ownedProfileActive };
|
|
482
496
|
profile = parsed;
|
|
@@ -709,7 +723,7 @@ export function writeDesktop3pConfig(
|
|
|
709
723
|
const metadata = parseMetadata(join(inspected.libraryPath, "_meta.json"));
|
|
710
724
|
if (!isOwnedDesktopGatewayEntry(metadata.entries.find(entry => entry.id === inspected.appliedId))) return refused;
|
|
711
725
|
try {
|
|
712
|
-
const selected = JSON.parse(
|
|
726
|
+
const selected = JSON.parse(readDesktopLibraryText(inspected.selectedProfilePath)) as Record<string, unknown>;
|
|
713
727
|
mode = selected.modelDiscoveryEnabled !== true ? "static"
|
|
714
728
|
: Array.isArray(selected.inferenceModels) ? "hybrid" : "discovery";
|
|
715
729
|
} catch { return refused; }
|
|
@@ -825,7 +839,7 @@ function writeDesktop3pConfigWithGenerator(
|
|
|
825
839
|
const freshMetadata = parseMetadata(metadataPath);
|
|
826
840
|
if (freshMetadata.appliedId !== refreshOnly.requiredId
|
|
827
841
|
|| !isOwnedDesktopGatewayEntry(freshMetadata.entries.find(entry => entry.id === refreshOnly.requiredId))
|
|
828
|
-
|| createHash("sha256").update(
|
|
842
|
+
|| createHash("sha256").update(readDesktopLibraryBytes(configPath)).digest("hex").slice(0, 16) !== refreshOnly.expectedFingerprint) {
|
|
829
843
|
throw new Error();
|
|
830
844
|
}
|
|
831
845
|
} catch { throw new Error("desktop_refresh_only_skipped"); }
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
isOwnedDesktopGatewayEntry,
|
|
17
17
|
parseMetadata,
|
|
18
18
|
profilePath,
|
|
19
|
+
readDesktopLibraryText,
|
|
19
20
|
resolveDesktop3pConfigLibraryPath,
|
|
20
21
|
SAFE_DESKTOP_PROFILE_ID,
|
|
21
22
|
type Desktop3pConfigLibraryOptions,
|
|
@@ -73,6 +74,12 @@ function readFileSnapshot(path: string): { exists: boolean; content?: string } {
|
|
|
73
74
|
return existsSync(path) ? { exists: true, content: readFileSync(path, "utf8") } : { exists: false };
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// Desktop library paths go through the verified regular-file reader: a swapped FIFO or symlink
|
|
78
|
+
// must surface as a refusal, never as a blocking open. Our own picker state file keeps the plain read.
|
|
79
|
+
function readLibrarySnapshot(path: string): { exists: boolean; content?: string } {
|
|
80
|
+
return existsSync(path) ? { exists: true, content: readDesktopLibraryText(path) } : { exists: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
function restoreFile(path: string, snapshot: { exists: boolean; content?: string }): void {
|
|
77
84
|
if (snapshot.exists) {
|
|
78
85
|
rollbackAtomicWriteFile(path, snapshot.content ?? "");
|
|
@@ -98,7 +105,7 @@ function pickerEntry(metadata: Desktop3pMetadata): Desktop3pMetadataEntry | unde
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
function profileObject(path: string): Record<string, unknown> {
|
|
101
|
-
const parsed = JSON.parse(
|
|
108
|
+
const parsed = JSON.parse(readDesktopLibraryText(path)) as unknown;
|
|
102
109
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
103
110
|
throw new Error("picker_profile_unreadable");
|
|
104
111
|
}
|
|
@@ -175,10 +182,10 @@ function applyDesktopPickerProfileLocked(options: { proxyPort: number } & Deskto
|
|
|
175
182
|
const alreadySelected = selectedId === entryId;
|
|
176
183
|
const priorState = readPickerState(previousStatePath);
|
|
177
184
|
const previousAppliedId = alreadySelected ? priorState?.previousAppliedId ?? null : selectedId;
|
|
178
|
-
const oldMeta =
|
|
179
|
-
const oldProfile =
|
|
185
|
+
const oldMeta = readLibrarySnapshot(metaPath);
|
|
186
|
+
const oldProfile = readLibrarySnapshot(profile);
|
|
180
187
|
const backupPath = `${profile}.bak`;
|
|
181
|
-
const oldBackup =
|
|
188
|
+
const oldBackup = readLibrarySnapshot(backupPath);
|
|
182
189
|
const oldState = readFileSnapshot(previousStatePath);
|
|
183
190
|
const changed = !alreadySelected || !oldProfile.exists || oldProfile.content !== target;
|
|
184
191
|
if (!changed) return { ok: true, changed: false, path: profile };
|
|
@@ -33,7 +33,9 @@ export interface RenderedDesktopModel extends DesktopProfileModel {
|
|
|
33
33
|
// renderDesktopProfile() converts it to a letter-first wire id because current
|
|
34
34
|
// Claude Desktop strips terminal dates before comparing model identities.
|
|
35
35
|
const DATE_ALIAS = /^claude-opus-4-8-(202[6-9]\d{4}|203[0-5]\d{4})$/;
|
|
36
|
-
|
|
36
|
+
// Desktop 2.31226's effort fallback recognizes fable prefixes, while Opus
|
|
37
|
+
// requires an exact canonical id. Keep the suffix so set_model stays distinct.
|
|
38
|
+
const PROFILE_WIRE_PREFIX = "claude-fable-5-";
|
|
37
39
|
const LEGACY_YEAR = 2026;
|
|
38
40
|
const LEGACY_DAY_COUNT = 365;
|
|
39
41
|
const ALIAS_FIRST_YEAR = 2026;
|
|
@@ -144,6 +146,13 @@ export function desktopProfileWireAlias(alias: string): string {
|
|
|
144
146
|
return `${PROFILE_WIRE_PREFIX}p${slot.toString(36).padStart(3, "0")}`;
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/** Decode-only profile namespace emitted before the Desktop effort compatibility fix. */
|
|
150
|
+
export function legacyDesktopProfileWireAlias(alias: string): string {
|
|
151
|
+
return validDateAlias(alias)
|
|
152
|
+
? desktopProfileWireAlias(alias).replace(PROFILE_WIRE_PREFIX, "claude-opus-4-8-")
|
|
153
|
+
: alias;
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
export function parseDesktopProfile(value: unknown): DesktopProfile {
|
|
148
157
|
if (!isPlainObject(value)) throw new DesktopProfileError("must be an object");
|
|
149
158
|
// `appliedFingerprint`/`appliedAt` are written back by the apply route once a profile
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { chmodSync, lstatSync, mkdirSync,
|
|
1
|
+
import { chmodSync, lstatSync, mkdirSync, realpathSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
4
4
|
import { hardenSecretDir } from "../lib/windows-secret-acl";
|
|
5
|
-
import { isRecord, resolveDesktop3pConfigLibraryPath, SAFE_DESKTOP_PROFILE_ID, type Desktop3pMetadata } from "./desktop-3p-library";
|
|
5
|
+
import { isRecord, readDesktopLibraryBytes, resolveDesktop3pConfigLibraryPath, SAFE_DESKTOP_PROFILE_ID, type Desktop3pMetadata } from "./desktop-3p-library";
|
|
6
6
|
import { DesktopStoreError, digest } from "./desktop-remote-store-state";
|
|
7
7
|
|
|
8
8
|
export const MAX_DESKTOP_METADATA_ENTRIES = 256;
|
|
@@ -43,7 +43,9 @@ export class StoreIO {
|
|
|
43
43
|
this.seenSizes.set(path, Math.max(counted, stat.size));
|
|
44
44
|
if (this.bytes > 8 * 1024 * 1024) throw new DesktopStoreError("unsafe");
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
// The shared verified regular-file read keeps a swapped FIFO from parking this
|
|
47
|
+
// open before the post-read identity recheck below can run.
|
|
48
|
+
const bytes = readDesktopLibraryBytes(path);
|
|
47
49
|
if (bytes.byteLength > maxBytes || identity(lstatSync(path)) !== identity(stat)) throw new DesktopStoreError("conflict");
|
|
48
50
|
const value: unknown = JSON.parse(bytes.toString("utf8"));
|
|
49
51
|
if (!isRecord(value)) throw new DesktopStoreError("unsafe");
|
package/src/claude/inbound.ts
CHANGED
|
@@ -24,7 +24,12 @@ import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The reference an Anthropic image block carries, as the Responses `image_url` string the bridge
|
|
29
|
+
* builds from it, or `null` for a source this route does not read. A file-backed source refuses.
|
|
30
|
+
* Shared with the direct Messages decoder (`src/protocols/codecs/messages-ir.ts`).
|
|
31
|
+
*/
|
|
32
|
+
export function anthropicImageBlockUrl(block: Rec): string | null {
|
|
28
33
|
const source = block.source;
|
|
29
34
|
if (!isRec(source)) return null;
|
|
30
35
|
if (source.type === "file") {
|
|
@@ -34,20 +39,25 @@ function imageBlockToInputImage(block: Rec): Rec | null {
|
|
|
34
39
|
}
|
|
35
40
|
if (source.type === "base64" && typeof source.data === "string") {
|
|
36
41
|
const media = typeof source.media_type === "string" ? source.media_type : "image/png";
|
|
37
|
-
return
|
|
42
|
+
return `data:${media};base64,${source.data}`;
|
|
38
43
|
}
|
|
39
44
|
if (source.type === "url" && typeof source.url === "string") {
|
|
40
|
-
return
|
|
45
|
+
return source.url;
|
|
41
46
|
}
|
|
42
47
|
return null;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
function
|
|
50
|
+
function imageBlockToInputImage(block: Rec): Rec | null {
|
|
51
|
+
const url = anthropicImageBlockUrl(block);
|
|
52
|
+
return url === null ? null : { type: "input_image", image_url: url };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function documentTitle(block: Rec): string | undefined {
|
|
46
56
|
return typeof block.title === "string" && block.title.length > 0 ? block.title : undefined;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
/**
|
|
50
|
-
function
|
|
59
|
+
/** A base64 document's bytes as a `data:` URL plus its title, or `null` for any other source. */
|
|
60
|
+
export function anthropicDocumentData(block: Rec): { fileData: string; title?: string } | null {
|
|
51
61
|
const source = block.source;
|
|
52
62
|
if (!isRec(source) || source.type !== "base64") return null;
|
|
53
63
|
const mediaType = typeof source.media_type === "string" && source.media_type.length > 0
|
|
@@ -55,10 +65,17 @@ function documentBlockToInputFile(block: Rec): Rec | null {
|
|
|
55
65
|
: "application/octet-stream";
|
|
56
66
|
if (typeof source.data !== "string" || source.data.length === 0) return null;
|
|
57
67
|
const title = documentTitle(block);
|
|
68
|
+
return { fileData: `data:${mediaType};base64,${source.data}`, ...(title !== undefined ? { title } : {}) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** An Anthropic base64 document as the Responses `input_file` block that carries its bytes. */
|
|
72
|
+
function documentBlockToInputFile(block: Rec): Rec | null {
|
|
73
|
+
const document = anthropicDocumentData(block);
|
|
74
|
+
if (!document) return null;
|
|
58
75
|
return {
|
|
59
76
|
type: "input_file",
|
|
60
|
-
file_data:
|
|
61
|
-
...(title !== undefined ? { filename: title } : {}),
|
|
77
|
+
file_data: document.fileData,
|
|
78
|
+
...(document.title !== undefined ? { filename: document.title } : {}),
|
|
62
79
|
};
|
|
63
80
|
}
|
|
64
81
|
|
|
@@ -145,7 +162,7 @@ const NO_ELISION: SkillElisionContext = { callIds: new Set(), names: [] };
|
|
|
145
162
|
* `Base directory for this skill: <dir>/<skill-name>`. Stub that block when the
|
|
146
163
|
* directory basename matches a blocked skill.
|
|
147
164
|
*/
|
|
148
|
-
function maybeElideSkillText(text: string, names: readonly string[]): string {
|
|
165
|
+
export function maybeElideSkillText(text: string, names: readonly string[]): string {
|
|
149
166
|
if (names.length === 0 || text.length < SKILL_ELISION_MIN_CHARS) return text;
|
|
150
167
|
if (!text.startsWith(SKILL_TEXT_MARKER)) return text;
|
|
151
168
|
const pathStart = SKILL_TEXT_MARKER.length;
|
|
@@ -162,14 +179,14 @@ function maybeElideSkillText(text: string, names: readonly string[]): string {
|
|
|
162
179
|
+ "(claudeCode.blockedSkills). The skill is loaded; answer from general knowledge instead of citing the bundle.";
|
|
163
180
|
}
|
|
164
181
|
|
|
165
|
-
function skillElisionStub(callId: string): string {
|
|
182
|
+
export function skillElisionStub(callId: string): string {
|
|
166
183
|
return "[opencodex] Skill document bundle elided for routed models (claudeCode.blockedSkills). "
|
|
167
184
|
+ `The skill loaded, but its reference documents were removed to save context (call ${callId}). `
|
|
168
185
|
+ "Answer from general knowledge instead of citing the bundle.";
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
/** Collect Skill-tool call ids whose input names a blocked skill. */
|
|
172
|
-
function blockedSkillCallIds(messages: readonly unknown[], blocked: readonly string[]): Set<string> {
|
|
189
|
+
export function blockedSkillCallIds(messages: readonly unknown[], blocked: readonly string[]): Set<string> {
|
|
173
190
|
const ids = new Set<string>();
|
|
174
191
|
if (blocked.length === 0) return ids;
|
|
175
192
|
const needles = blocked.map(name => name.toLowerCase()).filter(name => name.length > 0);
|
|
@@ -222,7 +239,7 @@ export function anthropicBodyElidesBlockedSkill(body: unknown, cc?: Pick<OcxClau
|
|
|
222
239
|
* prefix and rotating the Desktop `prompt_cache_key` fallback below on every turn
|
|
223
240
|
* (#4148).
|
|
224
241
|
*/
|
|
225
|
-
function systemMessageText(content: unknown): string {
|
|
242
|
+
export function systemMessageText(content: unknown): string {
|
|
226
243
|
if (typeof content === "string") return content;
|
|
227
244
|
if (!Array.isArray(content)) return "";
|
|
228
245
|
const parts: string[] = [];
|
|
@@ -410,19 +427,10 @@ function translateAnthropicRequest(
|
|
|
410
427
|
cc: OcxClaudeCodeConfig | undefined,
|
|
411
428
|
budget: TranslatorBudget,
|
|
412
429
|
): ClaudeInboundTranslation {
|
|
413
|
-
|
|
414
|
-
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
415
|
-
throw new AnthropicRequestError("model is required");
|
|
416
|
-
}
|
|
417
|
-
if (!Array.isArray(raw.messages) || raw.messages.length === 0) {
|
|
418
|
-
throw new AnthropicRequestError("messages must be a non-empty array");
|
|
419
|
-
}
|
|
430
|
+
assertAnthropicRequestShape(raw);
|
|
420
431
|
|
|
421
432
|
const input: Rec[] = [];
|
|
422
433
|
const nativeReasoningReplay = new Map<string, string>();
|
|
423
|
-
const systemParts: string[] = [];
|
|
424
|
-
const topLevelSystem = systemToInstructions(raw.system);
|
|
425
|
-
if (topLevelSystem !== undefined) systemParts.push(topLevelSystem);
|
|
426
434
|
const blockedNames = effectiveBlockedSkillNames(cc);
|
|
427
435
|
const elide: SkillElisionContext = {
|
|
428
436
|
callIds: blockedSkillCallIds(raw.messages, blockedNames),
|
|
@@ -451,36 +459,99 @@ function translateAnthropicRequest(
|
|
|
451
459
|
stream: raw.stream === true,
|
|
452
460
|
};
|
|
453
461
|
|
|
462
|
+
const system = anthropicSystemLayout(raw.system, cc);
|
|
463
|
+
if (system.instructions !== undefined) body.instructions = system.instructions;
|
|
464
|
+
if (system.dynamicNotice !== undefined) {
|
|
465
|
+
input.push({
|
|
466
|
+
type: "message",
|
|
467
|
+
role: "user",
|
|
468
|
+
content: [{ type: "input_text", text: system.dynamicNotice }],
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const tools = toolsToResponses(raw.tools);
|
|
473
|
+
if (tools) body.tools = tools;
|
|
474
|
+
const cacheKeySource = applyAnthropicSettings(raw, body, system.cacheSystem, tools);
|
|
475
|
+
return { body, cacheKeySource, nativeReasoningReplay };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function assertAnthropicRequestShape(raw: unknown): asserts raw is Rec & { model: string; messages: unknown[] } {
|
|
479
|
+
if (!isRec(raw)) throw new AnthropicRequestError("request body must be a JSON object");
|
|
480
|
+
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
481
|
+
throw new AnthropicRequestError("model is required");
|
|
482
|
+
}
|
|
483
|
+
if (!Array.isArray(raw.messages) || raw.messages.length === 0) {
|
|
484
|
+
throw new AnthropicRequestError("messages must be a non-empty array");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Where the top-level `system` lands on the bridge body, and what the cache-cohort key hashes. */
|
|
489
|
+
export interface AnthropicSystemLayout {
|
|
490
|
+
/** The Responses `instructions`, when there are any. */
|
|
491
|
+
instructions?: string;
|
|
492
|
+
/** A dynamic notice `stabilizePromptCache` moved out of the system text into a trailing user turn. */
|
|
493
|
+
dynamicNotice?: string;
|
|
494
|
+
cacheSystem: string | string[];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The system text placement `claudeCode.stabilizePromptCache` decides; shared with the direct decoder. */
|
|
498
|
+
export function anthropicSystemLayout(system: unknown, cc: OcxClaudeCodeConfig | undefined): AnthropicSystemLayout {
|
|
499
|
+
const systemParts: string[] = [];
|
|
500
|
+
const topLevelSystem = systemToInstructions(system);
|
|
501
|
+
if (topLevelSystem !== undefined) systemParts.push(topLevelSystem);
|
|
454
502
|
const joinedSystem = systemParts.length > 0 ? systemParts.join("\n\n") : "";
|
|
455
503
|
const stabilizePromptCache = cc?.stabilizePromptCache === true;
|
|
456
504
|
// Desktop fallback hashes raw systemParts unless the caller opted into
|
|
457
505
|
// harness cleanup. Opt-in then hashes the same string as body.instructions.
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
content: [{ type: "input_text", text: stabilized.dynamicNotice }],
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
cacheSystem = stabilized.instructions;
|
|
477
|
-
} else {
|
|
478
|
-
body.instructions = joinedSystem;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
506
|
+
if (!joinedSystem) return { cacheSystem: systemParts };
|
|
507
|
+
if (!stabilizePromptCache) return { instructions: joinedSystem, cacheSystem: systemParts };
|
|
508
|
+
// Claude Code appends growing <total_tokens>N tokens left</total_tokens>
|
|
509
|
+
// footers (and occasional TaskCreate nudges) into system text. That churn
|
|
510
|
+
// breaks Muse/Go prefix cache on the Responses instructions prefix even
|
|
511
|
+
// when tools stay stable. Relocation is caller-opted, not inferred from
|
|
512
|
+
// a matching suffix or metadata.user_id.
|
|
513
|
+
const stabilized = stabilizeClaudeInstructionsForPromptCache(joinedSystem);
|
|
514
|
+
return {
|
|
515
|
+
...(stabilized.instructions ? { instructions: stabilized.instructions } : {}),
|
|
516
|
+
// Token footers are dropped; append nothing if no other notice remains.
|
|
517
|
+
...(stabilized.dynamicNotice ? { dynamicNotice: stabilized.dynamicNotice } : {}),
|
|
518
|
+
cacheSystem: stabilized.instructions,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
481
521
|
|
|
482
|
-
|
|
483
|
-
|
|
522
|
+
/**
|
|
523
|
+
* The request settings of a Messages body as a Responses body with no conversation: `input` is
|
|
524
|
+
* empty and there are no `instructions` or `tools`, but the prompt-cache key is still derived
|
|
525
|
+
* from the system text and translated tool catalog exactly as the bridge derives it. The
|
|
526
|
+
* Antigravity direct lane hands this to the Responses core while the conversation is decoded
|
|
527
|
+
* straight into the IR (`src/protocols/codecs/messages-ir.ts`); both bodies are completed by
|
|
528
|
+
* `applyAnthropicSettings`. It validates the same request shape, but a conversation error the
|
|
529
|
+
* bridge would report first is not looked for here: a caller that gets a throw must translate
|
|
530
|
+
* the full body to learn the bridge's answer.
|
|
531
|
+
*/
|
|
532
|
+
export function anthropicSettingsTranslation(
|
|
533
|
+
raw: unknown,
|
|
534
|
+
cc?: OcxClaudeCodeConfig,
|
|
535
|
+
): { body: Rec; cacheKeySource: ClaudeCacheKeySource } {
|
|
536
|
+
assertAnthropicRequestShape(raw);
|
|
537
|
+
const body: Rec = {
|
|
538
|
+
model: resolveInboundModel(raw.model, cc),
|
|
539
|
+
input: [],
|
|
540
|
+
store: false,
|
|
541
|
+
stream: raw.stream === true,
|
|
542
|
+
};
|
|
543
|
+
const system = anthropicSystemLayout(raw.system, cc);
|
|
544
|
+
const cacheKeySource = applyAnthropicSettings(raw, body, system.cacheSystem, toolsToResponses(raw.tools));
|
|
545
|
+
return { body, cacheKeySource };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Everything after the conversation and tool catalog: choice, limits, format, cache key, reasoning. */
|
|
549
|
+
function applyAnthropicSettings(
|
|
550
|
+
raw: Rec,
|
|
551
|
+
body: Rec,
|
|
552
|
+
cacheSystem: string | string[],
|
|
553
|
+
tools: Rec[] | undefined,
|
|
554
|
+
): ClaudeCacheKeySource {
|
|
484
555
|
toolChoiceToResponses(raw.tool_choice, body);
|
|
485
556
|
|
|
486
557
|
if (typeof raw.max_tokens === "number") body.max_output_tokens = raw.max_tokens;
|
|
@@ -505,7 +576,7 @@ function translateAnthropicRequest(
|
|
|
505
576
|
// per-session key with a bounded length/charset.
|
|
506
577
|
body.prompt_cache_key = userIdHash.slice(0, 32);
|
|
507
578
|
cacheKeySource = "metadata";
|
|
508
|
-
} else if (
|
|
579
|
+
} else if (typeof cacheSystem === "string" || cacheSystem.length > 0) {
|
|
509
580
|
// Claude Desktop sends no metadata.user_id (H1, devlog 130): without any key the
|
|
510
581
|
// ChatGPT/OpenAI backends reported cached_tokens:0 on every turn. Fall back to a
|
|
511
582
|
// cache-cohort hash (devlog 260712 B4 + Pro review 012): fingerprint what the
|
|
@@ -526,7 +597,7 @@ function translateAnthropicRequest(
|
|
|
526
597
|
version: 2,
|
|
527
598
|
model: body.model,
|
|
528
599
|
system: cacheSystem,
|
|
529
|
-
tools:
|
|
600
|
+
tools: tools ?? [],
|
|
530
601
|
}))
|
|
531
602
|
.digest("hex").slice(0, 32);
|
|
532
603
|
cacheKeySource = "system";
|
|
@@ -556,7 +627,7 @@ function translateAnthropicRequest(
|
|
|
556
627
|
body.include = ["reasoning.encrypted_content"];
|
|
557
628
|
}
|
|
558
629
|
|
|
559
|
-
return
|
|
630
|
+
return cacheKeySource;
|
|
560
631
|
}
|
|
561
632
|
|
|
562
633
|
/**
|
|
@@ -197,7 +197,9 @@ export interface AuthorityOptions {
|
|
|
197
197
|
permittedDnsNames?: readonly string[];
|
|
198
198
|
/** With permittedDnsNames: also exclude every IP address (default true). */
|
|
199
199
|
excludeAllIpAddresses?: boolean;
|
|
200
|
-
|
|
200
|
+
/** Restrict this authority to TLS server authentication; legacy callers keep their existing scope. */
|
|
201
|
+
serverAuthOnly?: boolean;
|
|
202
|
+
}
|
|
201
203
|
|
|
202
204
|
export function createCertificateAuthority(options: AuthorityOptions): LocalInterceptCa {
|
|
203
205
|
const validityDays = options.validityDays ?? CA_VALIDITY_DAYS;
|
|
@@ -217,6 +219,7 @@ export function createCertificateAuthority(options: AuthorityOptions): LocalInte
|
|
|
217
219
|
// keyCertSign | cRLSign
|
|
218
220
|
extension(OID.keyUsage, true, bitString(Uint8Array.of(0x06), 1)),
|
|
219
221
|
extension(OID.subjectKeyIdentifier, false, octetString(keyIdentifier(publicKey))),
|
|
222
|
+
...(options.serverAuthOnly ? [extension(OID.extendedKeyUsage, true, sequence(objectIdentifier(OID.serverAuth)))] : []),
|
|
220
223
|
...(options.permittedDnsNames?.length
|
|
221
224
|
? [extension(OID.nameConstraints, true, nameConstraints(options.permittedDnsNames, options.excludeAllIpAddresses !== false))]
|
|
222
225
|
: []),
|
|
@@ -234,6 +237,10 @@ export function createLocalInterceptCa(): LocalInterceptCa {
|
|
|
234
237
|
return createCertificateAuthority({ commonName: CLAUDE_INTERCEPT_CA_COMMON_NAME });
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
export function isServerAuthOnlyCertificate(certificate: X509Certificate): boolean {
|
|
241
|
+
return certificate.keyUsage?.length === 1 && certificate.keyUsage[0] === OID.serverAuth;
|
|
242
|
+
}
|
|
243
|
+
|
|
237
244
|
/**
|
|
238
245
|
* Test hook for trust-boundary suites: mint a self-signed authority carrying an arbitrary list of
|
|
239
246
|
* DER-encoded Extension items so adversarial profiles still bear a valid signature. Production
|