@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { MAX_REMOTE_CATALOG_BYTES } from "../../server/catalog-download";
|
|
4
|
+
import { readBoundedResponseBytes } from "../../lib/bounded-body";
|
|
5
|
+
import { withCatalogWriteSerialization, type CatalogSerializationOutcome, type CatalogWritePermit } from "../catalog-write-serialization";
|
|
6
|
+
import { replaceActiveCodexCatalog } from "../internal/catalog-writer";
|
|
7
|
+
import { resetCodexAppServerCatalogStateCache } from "../app-server-processes";
|
|
8
|
+
import { getCodexHome } from "../paths";
|
|
9
|
+
import { readCodexCatalogPathForHome } from "./parsing";
|
|
10
|
+
import { invalidateCodexModelsCacheWithPermit } from "./sync";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
13
|
+
const MAX_MODELS = 2_000;
|
|
14
|
+
const MAX_SLUG_BYTES = 512;
|
|
15
|
+
const ALLOWED_MODALITIES = new Set(["text", "image", "audio"]);
|
|
16
|
+
|
|
17
|
+
export type RemoteCatalogFailureCode =
|
|
18
|
+
| "url_invalid" | "insecure_http_refused" | "credential_invalid" | "request_failed"
|
|
19
|
+
| "redirect_refused" | "http_error" | "body_too_large" | "body_invalid"
|
|
20
|
+
| "catalog_invalid" | "write_failed" | "lock_busy" | "lock_database" | "unsafe_path";
|
|
21
|
+
|
|
22
|
+
export class RemoteCatalogError extends Error {
|
|
23
|
+
constructor(readonly code: RemoteCatalogFailureCode, message: string, readonly status?: number) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "RemoteCatalogError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RemoteCatalogDocument extends Record<string, unknown> {
|
|
30
|
+
models: Record<string, unknown>[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PullRemoteCatalogOptions {
|
|
34
|
+
token?: string;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
maxBytes?: number;
|
|
37
|
+
fetchImpl?: typeof fetch;
|
|
38
|
+
codexHome?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PullRemoteCatalogResult {
|
|
42
|
+
status: "updated" | "unchanged";
|
|
43
|
+
catalogWritten: boolean;
|
|
44
|
+
cacheSynced: boolean;
|
|
45
|
+
codexHome: string;
|
|
46
|
+
catalogPath: string;
|
|
47
|
+
modelCount: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isLoopback(hostname: string): boolean {
|
|
51
|
+
const host = hostname.toLowerCase();
|
|
52
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function validateRemoteCatalogUrl(input: string): URL {
|
|
56
|
+
let url: URL;
|
|
57
|
+
try { url = new URL(input); } catch { throw new RemoteCatalogError("url_invalid", "Catalog URL must be an absolute HTTPS URL"); }
|
|
58
|
+
if (url.username || url.password) throw new RemoteCatalogError("url_invalid", "Catalog URL must not contain credentials");
|
|
59
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url.hostname))) {
|
|
60
|
+
throw new RemoteCatalogError("insecure_http_refused", "Catalog URL requires HTTPS (HTTP is allowed only on loopback)");
|
|
61
|
+
}
|
|
62
|
+
if (url.pathname !== "/v1/catalog" || url.search || url.hash) {
|
|
63
|
+
throw new RemoteCatalogError("url_invalid", "Catalog URL must identify /v1/catalog without query or fragment");
|
|
64
|
+
}
|
|
65
|
+
return url;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateToken(token: string | undefined): string | undefined {
|
|
69
|
+
if (token === undefined) return undefined;
|
|
70
|
+
if (!token || token.length > 4096 || /[\r\n\0]/.test(token)) {
|
|
71
|
+
throw new RemoteCatalogError("credential_invalid", "Catalog authentication environment variable is invalid");
|
|
72
|
+
}
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function validateRemoteCatalogDocument(value: unknown): RemoteCatalogDocument {
|
|
77
|
+
const invalid = (message: string): never => { throw new RemoteCatalogError("catalog_invalid", message); };
|
|
78
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid("Remote catalog must be a JSON object");
|
|
79
|
+
const document = value as Record<string, unknown>;
|
|
80
|
+
const rawModels = document.models;
|
|
81
|
+
if (!Array.isArray(rawModels) || rawModels.length === 0 || rawModels.length > MAX_MODELS) {
|
|
82
|
+
invalid("Remote catalog models must be a non-empty bounded array");
|
|
83
|
+
}
|
|
84
|
+
const models = rawModels as unknown[];
|
|
85
|
+
const slugs = new Set<string>();
|
|
86
|
+
for (const row of models) {
|
|
87
|
+
if (!row || typeof row !== "object" || Array.isArray(row) || Object.getPrototypeOf(row) !== Object.prototype) {
|
|
88
|
+
invalid("Remote catalog model rows must be plain objects");
|
|
89
|
+
}
|
|
90
|
+
const model = row as Record<string, unknown>;
|
|
91
|
+
const rawSlug = model.slug;
|
|
92
|
+
if (typeof rawSlug !== "string") invalid("Remote catalog contains an invalid model slug");
|
|
93
|
+
const slug = rawSlug as string;
|
|
94
|
+
if (slug !== slug.trim() || !slug
|
|
95
|
+
|| new TextEncoder().encode(slug).byteLength > MAX_SLUG_BYTES || /[\x00-\x1f\x7f]/.test(slug)) {
|
|
96
|
+
invalid("Remote catalog contains an invalid model slug");
|
|
97
|
+
}
|
|
98
|
+
if (slugs.has(slug)) invalid("Remote catalog contains duplicate model slugs");
|
|
99
|
+
slugs.add(slug);
|
|
100
|
+
if (Object.hasOwn(model, "input_modalities")) {
|
|
101
|
+
const modalities = model.input_modalities;
|
|
102
|
+
if (!Array.isArray(modalities) || modalities.length === 0
|
|
103
|
+
|| modalities.some(item => typeof item !== "string" || !ALLOWED_MODALITIES.has(item))) {
|
|
104
|
+
invalid("Remote catalog contains unsupported input modalities");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return document as RemoteCatalogDocument;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function safeTimeout(value: number | undefined): number {
|
|
112
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
113
|
+
? Math.min(Math.floor(value), 120_000) : DEFAULT_TIMEOUT_MS;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function fetchRemoteCatalog(
|
|
117
|
+
input: string,
|
|
118
|
+
options: Pick<PullRemoteCatalogOptions, "token" | "timeoutMs" | "maxBytes" | "fetchImpl"> = {},
|
|
119
|
+
): Promise<{ document: RemoteCatalogDocument; content: string }> {
|
|
120
|
+
const url = validateRemoteCatalogUrl(input);
|
|
121
|
+
const token = validateToken(options.token);
|
|
122
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
123
|
+
if (token !== undefined) headers.set("Authorization", `Bearer ${token}`);
|
|
124
|
+
let response: Response;
|
|
125
|
+
try {
|
|
126
|
+
response = await (options.fetchImpl ?? fetch)(url, {
|
|
127
|
+
method: "GET", headers, redirect: "manual", signal: AbortSignal.timeout(safeTimeout(options.timeoutMs)),
|
|
128
|
+
});
|
|
129
|
+
} catch {
|
|
130
|
+
throw new RemoteCatalogError("request_failed", "Remote catalog request did not complete");
|
|
131
|
+
}
|
|
132
|
+
if (response.status >= 300 && response.status < 400) {
|
|
133
|
+
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
134
|
+
throw new RemoteCatalogError("redirect_refused", "Remote catalog redirect was refused", response.status);
|
|
135
|
+
}
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
138
|
+
throw new RemoteCatalogError("http_error", `Remote catalog request failed with HTTP ${response.status}`, response.status);
|
|
139
|
+
}
|
|
140
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
141
|
+
if (contentType !== "application/json" && contentType?.endsWith("+json") !== true) {
|
|
142
|
+
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
143
|
+
throw new RemoteCatalogError("body_invalid", "Remote catalog response was not JSON");
|
|
144
|
+
}
|
|
145
|
+
const maxBytes = options.maxBytes ?? MAX_REMOTE_CATALOG_BYTES;
|
|
146
|
+
const declaredRaw = response.headers.get("content-length");
|
|
147
|
+
if (declaredRaw !== null) {
|
|
148
|
+
const declared = Number(declaredRaw);
|
|
149
|
+
if (!Number.isSafeInteger(declared) || declared < 0 || declared > maxBytes) {
|
|
150
|
+
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
151
|
+
throw new RemoteCatalogError("body_too_large", "Remote catalog exceeded the allowed size");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let bytes: Uint8Array;
|
|
155
|
+
try {
|
|
156
|
+
const bounded = await readBoundedResponseBytes(response, { maxBytes, inactivityTimeoutMs: safeTimeout(options.timeoutMs) });
|
|
157
|
+
if (bounded.oversized) throw new RemoteCatalogError("body_too_large", "Remote catalog exceeded the allowed size");
|
|
158
|
+
bytes = bounded.bytes;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof RemoteCatalogError) throw error;
|
|
161
|
+
throw new RemoteCatalogError("request_failed", "Remote catalog download did not complete");
|
|
162
|
+
}
|
|
163
|
+
let text: string;
|
|
164
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
|
165
|
+
catch { throw new RemoteCatalogError("body_invalid", "Remote catalog was not valid UTF-8"); }
|
|
166
|
+
let parsed: unknown;
|
|
167
|
+
try { parsed = JSON.parse(text); }
|
|
168
|
+
catch { throw new RemoteCatalogError("body_invalid", "Remote catalog was not valid JSON"); }
|
|
169
|
+
const document = validateRemoteCatalogDocument(parsed);
|
|
170
|
+
return { document, content: `${JSON.stringify(document, null, 2)}\n` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function mapSerializationFailure<T>(outcome: CatalogSerializationOutcome<T>): never {
|
|
174
|
+
if (outcome.kind === "completed") throw new RemoteCatalogError("write_failed", "Remote catalog installation failed");
|
|
175
|
+
const code = outcome.reason === "busy" ? "lock_busy" : outcome.reason === "database" ? "lock_database" : "unsafe_path";
|
|
176
|
+
throw new RemoteCatalogError(code, `Remote catalog installation unavailable (${outcome.reason})`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Put the catalog file back the way this pull found it.
|
|
181
|
+
*
|
|
182
|
+
* `replaceActiveCodexCatalog` is an atomic FILE write; the serialization permit rolls back the
|
|
183
|
+
* SQLite transaction and nothing on disk. Without this, a cache rebuild that fails after the
|
|
184
|
+
* catalog was replaced leaves a new catalog paired with a stale `models_cache.json` — the exact
|
|
185
|
+
* split the last-known-good guarantee exists to prevent — while the caller is told the pull
|
|
186
|
+
* failed and wrote nothing.
|
|
187
|
+
*/
|
|
188
|
+
function restorePreviousCatalog(
|
|
189
|
+
permit: CatalogWritePermit,
|
|
190
|
+
codexHome: string,
|
|
191
|
+
catalogPath: string,
|
|
192
|
+
previous: Buffer | null,
|
|
193
|
+
): void {
|
|
194
|
+
if (previous) {
|
|
195
|
+
replaceActiveCodexCatalog(permit, codexHome, { path: catalogPath, content: previous.toString("utf8") });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// There was no catalog before this pull, so last-known-good is its absence.
|
|
199
|
+
rmSync(catalogPath, { force: true });
|
|
200
|
+
resetCodexAppServerCatalogStateCache();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function pullRemoteCatalog(input: string, options: PullRemoteCatalogOptions = {}): Promise<PullRemoteCatalogResult> {
|
|
204
|
+
// Network acquisition and fail-closed validation intentionally happen before K.
|
|
205
|
+
const fetched = await fetchRemoteCatalog(input, options);
|
|
206
|
+
const codexHome = options.codexHome ?? getCodexHome();
|
|
207
|
+
const catalogPath = readCodexCatalogPathForHome(codexHome);
|
|
208
|
+
const current = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
|
|
209
|
+
const candidate = Buffer.from(fetched.content, "utf8");
|
|
210
|
+
if (current?.equals(candidate)) {
|
|
211
|
+
return { status: "unchanged", catalogWritten: false, cacheSynced: false, codexHome, catalogPath, modelCount: fetched.document.models.length };
|
|
212
|
+
}
|
|
213
|
+
const outcome = withCatalogWriteSerialization(codexHome, permit => {
|
|
214
|
+
// Re-check under K: another writer may have installed these bytes while the request was in flight.
|
|
215
|
+
const lockedCurrent = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
|
|
216
|
+
if (lockedCurrent?.equals(candidate)) return { catalogWritten: false, cacheSynced: false };
|
|
217
|
+
replaceActiveCodexCatalog(permit, codexHome, { path: catalogPath, content: fetched.content });
|
|
218
|
+
const cacheSynced = invalidateCodexModelsCacheWithPermit(permit, codexHome, { allowWhenDesiredDisabled: true });
|
|
219
|
+
if (!cacheSynced) {
|
|
220
|
+
restorePreviousCatalog(permit, codexHome, catalogPath, lockedCurrent);
|
|
221
|
+
throw new RemoteCatalogError("write_failed", "Remote catalog cache synchronization failed");
|
|
222
|
+
}
|
|
223
|
+
return { catalogWritten: true, cacheSynced: true };
|
|
224
|
+
});
|
|
225
|
+
if (outcome.kind !== "completed") return mapSerializationFailure(outcome);
|
|
226
|
+
return {
|
|
227
|
+
status: outcome.value.catalogWritten ? "updated" : "unchanged",
|
|
228
|
+
...outcome.value,
|
|
229
|
+
codexHome,
|
|
230
|
+
catalogPath,
|
|
231
|
+
modelCount: fetched.document.models.length,
|
|
232
|
+
};
|
|
233
|
+
}
|