@bitkyc08/opencodex 2.43.0 → 2.44.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.
Files changed (138) hide show
  1. package/README.md +25 -17
  2. package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
  3. package/gui/dist/assets/index-ltx3L-WS.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/command-code.ts +23 -2
  7. package/src/adapters/cursor/native-exec.ts +63 -11
  8. package/src/adapters/cursor/tool-definitions.ts +1 -1
  9. package/src/adapters/cursor/tool-schemas.ts +59 -0
  10. package/src/adapters/kiro.ts +54 -8
  11. package/src/adapters/openai-chat.ts +23 -6
  12. package/src/adapters/openai-responses.ts +3 -1
  13. package/src/adapters/opencode-go.ts +35 -0
  14. package/src/claude/agents-inject.ts +12 -5
  15. package/src/claude/desktop-3p-library.ts +89 -0
  16. package/src/claude/desktop-3p.ts +166 -95
  17. package/src/claude/desktop-discovery-inputs.ts +44 -0
  18. package/src/claude/desktop-profile.ts +1 -1
  19. package/src/claude/desktop-remote-store-artifact.ts +185 -0
  20. package/src/claude/desktop-remote-store-io.ts +91 -0
  21. package/src/claude/desktop-remote-store-state.ts +127 -0
  22. package/src/claude/desktop-remote-store.ts +269 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound-records.ts +7 -0
  25. package/src/claude/inbound.ts +1 -1
  26. package/src/claude/model-info.ts +16 -3
  27. package/src/cli/account-api.ts +7 -1
  28. package/src/cli/aside-profiles.ts +17 -0
  29. package/src/cli/capabilities.ts +39 -0
  30. package/src/cli/claude-agent-startup-sync.ts +3 -1
  31. package/src/cli/claude-desktop.ts +127 -18
  32. package/src/cli/connect.ts +27 -14
  33. package/src/cli/dispatch.ts +30 -16
  34. package/src/cli/export-command.ts +3 -1
  35. package/src/cli/index.ts +28 -16
  36. package/src/cli/integrations.ts +46 -12
  37. package/src/cli/opencode.ts +11 -4
  38. package/src/cli/runtime-api.ts +7 -2
  39. package/src/cli/uninstall-client-state.ts +78 -0
  40. package/src/client/connect.ts +476 -223
  41. package/src/client/hub-client.ts +81 -0
  42. package/src/client/hub-relay.ts +38 -6
  43. package/src/client/lifecycle-lock.ts +129 -0
  44. package/src/client/state.ts +71 -42
  45. package/src/clients/aside-profiles.ts +224 -0
  46. package/src/codex/account-lifecycle.ts +5 -9
  47. package/src/codex/auth-api.ts +50 -4
  48. package/src/codex/catalog/metadata.ts +0 -2
  49. package/src/codex/catalog/provider-fetch.ts +7 -5
  50. package/src/codex/catalog/sync.ts +88 -24
  51. package/src/codex/convergence.ts +2 -0
  52. package/src/codex/inject.ts +45 -14
  53. package/src/codex/journal.ts +6 -6
  54. package/src/codex/quota-refresh-outcome.ts +27 -0
  55. package/src/codex/shim.ts +7 -1
  56. package/src/combos/resolve.ts +10 -7
  57. package/src/config.ts +18 -5
  58. package/src/generated/compatibility-version.json +209 -105
  59. package/src/images/loop.ts +5 -2
  60. package/src/integrations/aside-profile-context.ts +270 -0
  61. package/src/integrations/aside-profile-journal.ts +229 -0
  62. package/src/integrations/aside-profiles.ts +176 -0
  63. package/src/integrations/catalog-refresh.ts +40 -0
  64. package/src/integrations/config-io.ts +16 -1
  65. package/src/integrations/owned-refresh.ts +14 -2
  66. package/src/integrations/state.ts +3 -1
  67. package/src/integrations/writer.ts +3 -1
  68. package/src/lab/fabric/producer-isolate.ts +84 -46
  69. package/src/lib/account-selection-events.ts +32 -0
  70. package/src/lib/errors.ts +9 -1
  71. package/src/lib/provider-outbound.ts +2 -45
  72. package/src/lib/proxy-env.ts +67 -0
  73. package/src/oauth/account-quota-rank.ts +9 -1
  74. package/src/oauth/anthropic-routing.ts +91 -23
  75. package/src/oauth/generic-account-failover.ts +15 -53
  76. package/src/oauth/index.ts +67 -68
  77. package/src/oauth/pool-settings-capability.ts +2 -1
  78. package/src/oauth/store.ts +91 -7
  79. package/src/oauth/types.ts +8 -0
  80. package/src/providers/api-key-selection.ts +110 -0
  81. package/src/providers/api-keys.ts +38 -31
  82. package/src/providers/context-cap.ts +27 -3
  83. package/src/providers/key-failover.ts +27 -17
  84. package/src/providers/provider-id-rewrite.ts +10 -8
  85. package/src/providers/registry.ts +1 -0
  86. package/src/responses/custom-tool-compat.ts +10 -0
  87. package/src/responses/function-call-compat.ts +173 -0
  88. package/src/responses/namespace-tool-compat.ts +27 -4
  89. package/src/responses/parser.ts +8 -0
  90. package/src/responses/task-input.ts +36 -0
  91. package/src/responses/tool-name-aliases.ts +79 -0
  92. package/src/router.ts +2 -0
  93. package/src/routing/capability.ts +10 -3
  94. package/src/routing/compatibility/assemble.ts +16 -1
  95. package/src/routing/evaluator.ts +5 -1
  96. package/src/server/auth-cors.ts +3 -0
  97. package/src/server/chat-native.ts +44 -9
  98. package/src/server/claude-messages.ts +71 -36
  99. package/src/server/grok-responses-snapshot-repair.ts +333 -0
  100. package/src/server/index.ts +26 -10
  101. package/src/server/management/account-selection-stream.ts +70 -0
  102. package/src/server/management/aside-profile-routes.ts +231 -0
  103. package/src/server/management/config-routes.ts +36 -41
  104. package/src/server/management/context.ts +3 -0
  105. package/src/server/management/integration-routes.ts +61 -3
  106. package/src/server/management/logs-usage-routes.ts +1 -0
  107. package/src/server/management/model-routes.ts +20 -9
  108. package/src/server/management/model-rows.ts +10 -2
  109. package/src/server/management/oauth-account-routes.ts +7 -0
  110. package/src/server/management/provider-routes.ts +10 -8
  111. package/src/server/management/route-registry.ts +20 -2
  112. package/src/server/management-auth.ts +16 -0
  113. package/src/server/relay-eager.ts +17 -5
  114. package/src/server/relay.ts +43 -4
  115. package/src/server/request-log.ts +29 -1
  116. package/src/server/responses/agent-task-recovery-cache.ts +11 -0
  117. package/src/server/responses/agent-task-recovery.ts +24 -4
  118. package/src/server/responses/codex-ws-pool.ts +4 -4
  119. package/src/server/responses/codex-ws-session.ts +2 -2
  120. package/src/server/responses/collaboration.ts +2 -1
  121. package/src/server/responses/combo-stream-preflight.ts +26 -7
  122. package/src/server/responses/core.ts +668 -151
  123. package/src/server/responses/fetch-helpers.ts +6 -1
  124. package/src/server/responses/ws-upstream.ts +8 -3
  125. package/src/server/responses-custom-tool-repair.ts +29 -9
  126. package/src/server/responses-function-tool-repair.ts +183 -0
  127. package/src/server/responses-snapshot-codec.ts +14 -0
  128. package/src/server/responses-snapshot-repair.ts +1 -13
  129. package/src/server/responses-undeclared-tool-guard.ts +1 -68
  130. package/src/server/sse-payload-rewrite.ts +3 -1
  131. package/src/service.ts +6 -2
  132. package/src/types/config.ts +23 -16
  133. package/src/types/provider.ts +14 -3
  134. package/src/types/request.ts +2 -0
  135. package/src/usage/log.ts +9 -0
  136. package/src/web-search/loop.ts +5 -2
  137. package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
  138. package/gui/dist/assets/index-Djowl68T.js +0 -112
@@ -1,13 +1,24 @@
1
+ import { readClientConnectionState } from "../client/state";
2
+ import { readServiceApiTokenState, readTokenBackupState } from "../lib/service-secrets";
3
+ import { withClientLifecycleSync, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
4
+ import { applyRemoteDesktopStore, inspectRemoteDesktopCleanup, restoreRemoteDesktopStore } from "./desktop-remote-store";
5
+ import { canonicalDirectory } from "./desktop-remote-store-io";
6
+ import {
7
+ resolveDesktop3pConfigLibraryPath, parseMetadata, SAFE_DESKTOP_PROFILE_ID, isRecord,
8
+ isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileForeignKeys,
9
+ type Desktop3pConfigLibraryOptions, type Desktop3pMetadata, type Desktop3pMetadataEntry,
10
+ } from "./desktop-3p-library";
11
+ export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
1
12
  import { createHash, randomUUID } from "node:crypto";
2
13
  import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
3
- import { homedir } from "node:os";
4
14
  import { join } from "node:path";
5
- import { atomicWriteFile } from "../config";
15
+ import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
16
+ import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
6
17
  import type { OcxClaudeDesktopProfile } from "../types";
7
- import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
8
18
  import {
9
19
  reconcileDesktopProfile,
10
20
  renderDesktopProfile,
21
+ validDateAlias,
11
22
  type DesktopProfileModel,
12
23
  } from "./desktop-profile";
13
24
  import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../codex/catalog";
@@ -50,33 +61,6 @@ export interface Desktop3pRoutedModel {
50
61
  */
51
62
  export const DESKTOP_SUPPORTS_1M_THRESHOLD = 1_000_000;
52
63
 
53
- export interface Desktop3pConfigLibraryOptions {
54
- env?: NodeJS.ProcessEnv;
55
- platform?: NodeJS.Platform;
56
- homeDir?: string;
57
- }
58
-
59
- /**
60
- * Resolve the config library from the same user-data root Claude Desktop uses. Keeping this in one
61
- * helper prevents the writer and dashboard status probe from agreeing on a path Desktop never reads.
62
- *
63
- * The resolution itself lives in `./desktop-3p-paths`, which ports Claude Desktop's own `GE()`
64
- * branch for branch — including the `-3p` suffix the app appends to its userData root. Dropping
65
- * that suffix points us at a directory Desktop never reads (GitHub #539).
66
- */
67
- export function resolveDesktop3pConfigLibraryPath(
68
- options: Desktop3pConfigLibraryOptions = {},
69
- ): string {
70
- if (options.env === undefined && options.platform === undefined && options.homeDir === undefined) {
71
- return claudeDesktopConfigLibraryDir();
72
- }
73
- return resolveConfigLibraryDir({
74
- env: options.env ?? process.env,
75
- platform: options.platform ?? process.platform,
76
- home: options.homeDir ?? homedir(),
77
- });
78
- }
79
-
80
64
  /** CLI arg parsing for `ocx claude desktop` mode flags (mutually exclusive). */
81
65
  export function parseDesktop3pModeArgs(flags: string[]): { mode: Desktop3pConfigMode } | { error: string } {
82
66
  const known = new Map<string, Desktop3pConfigMode>([
@@ -91,18 +75,6 @@ export function parseDesktop3pModeArgs(flags: string[]): { mode: Desktop3pConfig
91
75
  return { mode: picked[0] ?? "static" };
92
76
  }
93
77
 
94
- interface Desktop3pMetadataEntry {
95
- id: string;
96
- name: string;
97
- [key: string]: unknown;
98
- }
99
-
100
- interface Desktop3pMetadata {
101
- appliedId?: string;
102
- entries: Desktop3pMetadataEntry[];
103
- [key: string]: unknown;
104
- }
105
-
106
78
  export type Desktop3pLibraryKind =
107
79
  | "not_installed"
108
80
  | "standard"
@@ -144,6 +116,7 @@ export interface Desktop3pRemovalResult {
144
116
 
145
117
  let desktop3pRegistry = new Map<string, string>();
146
118
  let desktop3pAliasesByRoute = new Map<string, string>();
119
+ let desktop3pRealAnthropicIds = new Set<string>();
147
120
 
148
121
  /** Derive a stable letter-first, three-character base36 code from a route key. */
149
122
  export function deriveDesktop3pCode(route: string): string {
@@ -192,7 +165,7 @@ function collectDesktop3pModels(
192
165
  routedModels: Array<Desktop3pRoutedModel>,
193
166
  profile?: OcxClaudeDesktopProfile,
194
167
  nativeContextCap?: NativeContextLimitsInput,
195
- ): { models: Desktop3pModelEntry[]; registry: Map<string, string> } {
168
+ ): { models: Desktop3pModelEntry[]; registry: Map<string, string>; realAnthropicIds: Set<string> } {
196
169
  const registry = new Map<string, string>();
197
170
  const models: Desktop3pModelEntry[] = [];
198
171
  const candidates: Desktop3pRoutedModel[] = [
@@ -205,6 +178,9 @@ function collectDesktop3pModels(
205
178
  }),
206
179
  ...routedModels,
207
180
  ];
181
+ const realAnthropicIds = new Set(candidates
182
+ .filter(model => model.provider === "anthropic" && model.id.startsWith("claude-"))
183
+ .map(model => model.id));
208
184
 
209
185
  if (profile) {
210
186
  const profileModels = candidates.map(({ provider, id, contextWindow }) => ({
@@ -242,7 +218,7 @@ function collectDesktop3pModels(
242
218
  registry.set(legacy, model.route);
243
219
  }
244
220
  desktop3pAliasesByRoute = aliasesByRoute;
245
- return { models, registry };
221
+ return { models, registry, realAnthropicIds };
246
222
  }
247
223
 
248
224
  for (const { provider, id, contextWindow } of candidates) {
@@ -285,7 +261,7 @@ function collectDesktop3pModels(
285
261
 
286
262
  if (models[0]) models[0].isFamilyDefault = true;
287
263
  desktop3pAliasesByRoute = new Map(candidates.map(({ provider, id }) => [`${provider}/${id}`, desktop3pAlias(provider, id)]));
288
- return { models, registry };
264
+ return { models, registry, realAnthropicIds };
289
265
  }
290
266
 
291
267
  /** Build and install the registry used to decode Desktop aliases. */
@@ -295,8 +271,9 @@ export function buildDesktop3pRegistry(
295
271
  profile?: OcxClaudeDesktopProfile,
296
272
  nativeContextCap?: NativeContextLimitsInput,
297
273
  ): Map<string, string> {
298
- const { registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
274
+ const { registry, realAnthropicIds } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
299
275
  desktop3pRegistry = registry;
276
+ desktop3pRealAnthropicIds = realAnthropicIds;
300
277
  return registry;
301
278
  }
302
279
 
@@ -307,8 +284,9 @@ export function generateDesktop3pModels(
307
284
  profile?: OcxClaudeDesktopProfile,
308
285
  nativeContextCap?: NativeContextLimitsInput,
309
286
  ): Desktop3pModelEntry[] {
310
- const { models, registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
287
+ const { models, registry, realAnthropicIds } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
311
288
  desktop3pRegistry = registry;
289
+ desktop3pRealAnthropicIds = realAnthropicIds;
312
290
  return models;
313
291
  }
314
292
 
@@ -317,6 +295,21 @@ export function resolveDesktop3pAlias(alias: string): string | null {
317
295
  return desktop3pRegistry.get(alias) ?? null;
318
296
  }
319
297
 
298
+ /** Exact registered and identity-preserving catalog IDs precede synthetic syntax. */
299
+ export function isKnownDesktop3pModelId(id: string): boolean {
300
+ return desktop3pRegistry.has(id) || desktop3pRealAnthropicIds.has(id);
301
+ }
302
+
303
+ /** Only missing IDs in the emitted Desktop namespaces are managed-alias errors. */
304
+ export function isUnresolvedDesktop3pAlias(id: string): boolean {
305
+ if (isKnownDesktop3pModelId(id)) return false;
306
+ // Validate the managed base even when Fast routing is disabled. This checks
307
+ // identity only; it neither enables a tier nor strips an exact full catalog ID.
308
+ const base = id.endsWith("--fast") ? id.slice(0, -"--fast".length) : id;
309
+ if (isKnownDesktop3pModelId(base)) return false;
310
+ return validDateAlias(base) || /^claude-opus-4-(?:8-)?[a-z][a-z0-9]{2}$/.test(base);
311
+ }
312
+
320
313
  /** Alias selected by the installed profile registry, falling back to the legacy hash shape. */
321
314
  export function activeDesktop3pAlias(provider: string, modelId: string): string {
322
315
  return desktop3pAliasesByRoute.get(`${provider}/${modelId}`) ?? desktop3pAlias(provider, modelId);
@@ -363,32 +356,6 @@ export function generateDesktop3pConfig(
363
356
  };
364
357
  }
365
358
 
366
- function parseMetadata(path: string): Desktop3pMetadata {
367
- if (!existsSync(path)) return { entries: [] };
368
- const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<Desktop3pMetadata>;
369
- if (!Array.isArray(parsed.entries)) throw new Error("Claude Desktop 3P _meta.json has no entries array");
370
- return { ...parsed, entries: parsed.entries };
371
- }
372
-
373
- const SAFE_DESKTOP_PROFILE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
374
-
375
- function isRecord(value: unknown): value is Record<string, unknown> {
376
- return typeof value === "object" && value !== null && !Array.isArray(value);
377
- }
378
-
379
- function isOwnedDesktopEntry(entry: Desktop3pMetadataEntry | undefined): boolean {
380
- return entry?.name === "opencodex" || entry?.name === "opencodex-standard";
381
- }
382
-
383
- /** A gateway row is removable; the selected standard row must always remain. */
384
- function isOwnedDesktopGatewayEntry(entry: Desktop3pMetadataEntry | undefined): boolean {
385
- return entry?.name === "opencodex";
386
- }
387
-
388
- function profilePath(libraryPath: string, id: string): string {
389
- return join(libraryPath, `${id}.json`);
390
- }
391
-
392
359
  /**
393
360
  * Read Desktop's selected config without changing its library.
394
361
  *
@@ -481,7 +448,76 @@ export function inspectDesktop3pConfigLibrary(
481
448
  * (or any other benign mismatch), while the Integrations card still showed the
482
449
  * leftover profile as applied/stale.
483
450
  */
451
+ /** Keep failures diagnosable without reflecting paths, config bytes or arbitrary errors. */
452
+ function desktopMutationFailureReason(error: unknown): string {
453
+ const code = error !== null && typeof error === "object" && "code" in error ? error.code : undefined;
454
+ switch (code) {
455
+ case "client_lifecycle_busy": return "client_lifecycle_busy";
456
+ case "client_lifecycle_lock_failed": return "client_lifecycle_lock_failed";
457
+ case "CONFIG_MUTATION_LOCK_UNAVAILABLE": return "config_mutation_lock_unavailable";
458
+ case "EACCES":
459
+ case "EPERM": return "desktop_filesystem_denied";
460
+ case "ENOENT": return "desktop_path_missing";
461
+ case "EBUSY": return "desktop_filesystem_busy";
462
+ default: return "desktop_mutation_failed";
463
+ }
464
+ }
465
+
484
466
  export function removeDesktop3pStandardPivot(
467
+ options: Desktop3pConfigLibraryOptions & {
468
+ appliedFingerprint?: string | null; unlink?: (path: string) => void;
469
+ lifecycleLockDeps?: ClientLifecycleLockDeps;
470
+ } = {},
471
+ ): Desktop3pRemovalResult {
472
+ const libraryPath = resolveDesktop3pConfigLibraryPath(options);
473
+ try {
474
+ return withClientLifecycleSync<Desktop3pRemovalResult>(held => withConfigMutationLockSync(() => {
475
+ const connection = readClientConnectionState();
476
+ if (connection.kind === "invalid" || connection.kind === "mismatched") {
477
+ return { ok: false, changed: false, kind: "unsafe", libraryPath, reason: `desktop_client_state_${connection.kind}` };
478
+ }
479
+ if (connection.kind === "connected"
480
+ && canonicalDirectory(libraryPath) !== canonicalDirectory(resolveDesktop3pConfigLibraryPath())) {
481
+ return { ok: false, changed: false, kind: "unsafe", libraryPath, reason: "desktop_library_identity_changed" };
482
+ }
483
+ const latest = readConfigDiagnostics();
484
+ if (latest.source === "fallback") return { ok: false, changed: false, kind: "unsafe", libraryPath, reason: "desktop_config_invalid" };
485
+ if (claudeDesktopIntegrationEnabled(latest.config)) {
486
+ const observed = inspectDesktop3pConfigLibrary(options);
487
+ if (observed.kind === "not_installed" || observed.kind === "no_owned_state") {
488
+ return { ok: true, changed: false, kind: "noop", libraryPath };
489
+ }
490
+ return { ok: false, changed: false, kind: "unsafe", libraryPath, reason: "desired_state_changed" };
491
+ }
492
+ const cleanup = inspectRemoteDesktopCleanup();
493
+ if (cleanup.kind === "absent") {
494
+ if (connection.kind === "disconnected") return removeDesktop3pStandardPivotLocal(options);
495
+ const client = connection.value;
496
+ const known = [readServiceApiTokenState(), readTokenBackupState()].flatMap(token => token.kind === "present" ? [token.fingerprint] : []);
497
+ const result = restoreRemoteDesktopStore(held, {
498
+ owner: { serverUrl: new URL(client.serverUrl).origin, apiKeyId: client.apiKeyId, connectedAt: client.connectedAt },
499
+ knownTokenFingerprints: known,
500
+ });
501
+ return result.ok
502
+ ? { ok: true, changed: result.changed, kind: result.changed ? "removed" : "noop", libraryPath }
503
+ : { ok: false, changed: result.changed, kind: "unsafe", reason: result.reason, libraryPath };
504
+ }
505
+ if (cleanup.kind === "unsafe" || canonicalDirectory(libraryPath) !== canonicalDirectory(resolveDesktop3pConfigLibraryPath())) {
506
+ return { ok: false, changed: false, kind: "unsafe", libraryPath };
507
+ }
508
+ const known = [readServiceApiTokenState(), readTokenBackupState()]
509
+ .flatMap(token => token.kind === "present" ? [token.fingerprint] : []);
510
+ const result = restoreRemoteDesktopStore(held, { owner: cleanup.owner, knownTokenFingerprints: known });
511
+ return result.ok
512
+ ? { ok: true, changed: result.changed, kind: result.changed ? "removed" : "noop", libraryPath }
513
+ : { ok: false, changed: result.changed, kind: result.reason === "cleanup_pending" ? "cleanup_incomplete" : "unsafe", reason: result.reason, libraryPath };
514
+ }), options.lifecycleLockDeps);
515
+ } catch (error) {
516
+ return { ok: false, changed: false, kind: "write_failed", libraryPath, reason: desktopMutationFailureReason(error) };
517
+ }
518
+ }
519
+
520
+ function removeDesktop3pStandardPivotLocal(
485
521
  options: Desktop3pConfigLibraryOptions & {
486
522
  appliedFingerprint?: string | null;
487
523
  unlink?: (path: string) => void;
@@ -554,8 +590,8 @@ export function removeDesktop3pStandardPivot(
554
590
  JSON.stringify({ ...metadataAfterPivot, entries: metadataAfterPivot.entries.filter(entry => !targetIds.includes(entry.id)) }, null, 2) + "\n",
555
591
  );
556
592
  return { ok: true, changed: true, kind: "removed", libraryPath: inspected.libraryPath };
557
- } catch {
558
- return { ok: false, changed: false, kind: "write_failed", libraryPath: inspected.libraryPath };
593
+ } catch (error) {
594
+ return { ok: false, changed: false, kind: "write_failed", libraryPath: inspected.libraryPath, reason: desktopMutationFailureReason(error) };
559
595
  }
560
596
  }
561
597
 
@@ -568,6 +604,59 @@ export function writeDesktop3pConfig(
568
604
  mode: Desktop3pConfigMode = "static",
569
605
  profile?: OcxClaudeDesktopProfile,
570
606
  nativeContextCap?: NativeContextLimitsInput,
607
+ lifecycleLockDeps?: ClientLifecycleLockDeps,
608
+ ): { written: boolean; path: string; reason?: string; fingerprint?: string } {
609
+ try {
610
+ return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
611
+ const connection = readClientConnectionState();
612
+ if (connection.kind === "invalid" || connection.kind === "mismatched") {
613
+ return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: `desktop_client_state_${connection.kind}` };
614
+ }
615
+ const latest = readConfigDiagnostics();
616
+ if (latest.source === "fallback") return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_config_invalid" };
617
+ if (!claudeDesktopIntegrationEnabled(latest.config)) {
618
+ return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desired_state_changed" };
619
+ }
620
+ if (connection.kind === "connected" || inspectRemoteDesktopCleanup().kind !== "absent") {
621
+ return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_remote_store_active" };
622
+ }
623
+ return writeDesktop3pConfigWithGenerator(() => (
624
+ generateDesktop3pConfig(port, nativeSlugs, routedModels, apiKey, mode, profile, nativeContextCap)
625
+ ));
626
+ }), lifecycleLockDeps);
627
+ } catch { return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_lifecycle_busy_or_unsafe" }; }
628
+ }
629
+
630
+ /** Write the hub's exact entries without constructing a client-local alias registry. */
631
+ export function writeRemoteDesktop3pConfig(options: {
632
+ baseUrl: string;
633
+ apiKey: string;
634
+ mode: Desktop3pConfigMode;
635
+ models: Desktop3pModelEntry[];
636
+ lifecycleLockDeps?: ClientLifecycleLockDeps;
637
+ }): { written: boolean; path: string; reason?: string; fingerprint?: string } {
638
+ try {
639
+ return withClientLifecycleSync(held => {
640
+ const connection = readClientConnectionState();
641
+ const token = readServiceApiTokenState();
642
+ if (connection.kind !== "connected" || token.kind !== "present") {
643
+ return { written: false, path: "", reason: "desktop_remote_connection_required" };
644
+ }
645
+ const client = connection.value;
646
+ const result = applyRemoteDesktopStore(held, {
647
+ baseUrl: options.baseUrl, apiKey: options.apiKey, mode: options.mode, models: options.models,
648
+ owner: { serverUrl: new URL(client.serverUrl).origin, apiKeyId: client.apiKeyId, connectedAt: client.connectedAt },
649
+ expectedTokenFingerprint: token.fingerprint,
650
+ });
651
+ return result.ok
652
+ ? { written: result.status === "applied", path: result.path ?? "", fingerprint: result.fingerprint }
653
+ : { written: false, path: "", reason: result.reason };
654
+ }, options.lifecycleLockDeps);
655
+ } catch { return { written: false, path: "", reason: "desktop_lifecycle_busy_or_unsafe" }; }
656
+ }
657
+
658
+ function writeDesktop3pConfigWithGenerator(
659
+ generate: () => object,
571
660
  ): { written: boolean; path: string; reason?: string; fingerprint?: string } {
572
661
  const libraryPath = resolveDesktop3pConfigLibraryPath();
573
662
  const metadataPath = join(libraryPath, "_meta.json");
@@ -579,13 +668,13 @@ export function writeDesktop3pConfig(
579
668
  const selected = metadata.entries.find(entry => entry?.id === metadata.appliedId && isOwnedDesktopGatewayEntry(entry));
580
669
  const existing = selected ?? metadata.entries.find(entry => isOwnedDesktopGatewayEntry(entry) && typeof entry.id === "string");
581
670
  const id = existing?.id ?? randomUUID();
582
- configPath = join(libraryPath, `${id}.json`);
671
+ configPath = profilePath(libraryPath, id);
583
672
  const entry: Desktop3pMetadataEntry = existing ? { ...existing, id, name: "opencodex" } : { id, name: "opencodex" };
584
673
  const entries = existing
585
674
  ? metadata.entries.map(current => current === existing ? entry : current)
586
675
  : [...metadata.entries, entry];
587
676
 
588
- const generated = generateDesktop3pConfig(port, nativeSlugs, routedModels, apiKey, mode, profile, nativeContextCap);
677
+ const generated = generate();
589
678
  const preserved = readDesktopProfileForeignKeys(configPath);
590
679
  const configJson = JSON.stringify({ ...preserved, ...generated }, null, 2) + "\n";
591
680
  const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
@@ -604,24 +693,6 @@ export function writeDesktop3pConfig(
604
693
  }
605
694
  }
606
695
 
607
- const OPENCODEX_DESKTOP_PROFILE_KEYS = new Set([
608
- "inferenceProvider",
609
- "inferenceCredentialKind",
610
- "inferenceGatewayBaseUrl",
611
- "inferenceGatewayApiKey",
612
- "modelDiscoveryEnabled",
613
- "inferenceModels",
614
- ]);
615
-
616
- function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
617
- if (!existsSync(path)) return {};
618
- const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
619
- if (!isRecord(parsed)) throw new Error("Claude Desktop 3P profile is not a JSON object");
620
- return Object.fromEntries(
621
- Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
622
- );
623
- }
624
-
625
696
  /** Backup an existing owned config then atomically replace it. Exported for failure-path tests. */
626
697
  export function atomicReplaceDesktopConfig(
627
698
  path: string,
@@ -0,0 +1,44 @@
1
+ import type { OcxConfig } from "../types";
2
+ import {
3
+ filterCatalogVisibleModels,
4
+ nativeContextLimits,
5
+ orderForSubagents,
6
+ type CatalogModel,
7
+ type NativeContextLimits,
8
+ } from "../codex/catalog";
9
+ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../codex/catalog/native-models";
10
+ import {
11
+ availableAccountGatedNativeModels,
12
+ type CodexModelEntitlementSnapshot,
13
+ } from "../codex/model-entitlements";
14
+ import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
15
+ import { providerCodexAccountMode } from "../providers/registry";
16
+ import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
17
+
18
+ export interface DesktopDiscoveryInputs {
19
+ nativeSlugs: string[];
20
+ routedModels: CatalogModel[];
21
+ nativeContextCap: NativeContextLimits;
22
+ }
23
+
24
+ /** Project captured discovery state without reading caches or installing aliases. */
25
+ export function buildDesktopDiscoveryInputs(options: {
26
+ config: OcxConfig;
27
+ models: readonly CatalogModel[];
28
+ modelEntitlements: CodexModelEntitlementSnapshot;
29
+ desktopNativeCandidates: readonly string[];
30
+ }): DesktopDiscoveryInputs {
31
+ const { config, models, modelEntitlements, desktopNativeCandidates } = options;
32
+ const eligibleAccountIds = providerCodexAccountMode(
33
+ OPENAI_CODEX_PROVIDER_ID,
34
+ config.providers[OPENAI_CODEX_PROVIDER_ID],
35
+ ) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
36
+ const available = availableAccountGatedNativeModels(modelEntitlements, eligibleAccountIds);
37
+ return {
38
+ nativeSlugs: desktopNativeCandidates.filter(slug => (
39
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || available.has(slug)
40
+ )),
41
+ routedModels: orderForSubagents(filterCatalogVisibleModels([...models], config), config.subagentModels),
42
+ nativeContextCap: nativeContextLimits(config),
43
+ };
44
+ }
@@ -83,7 +83,7 @@ function isRealAnthropicRoute(route: string): boolean {
83
83
  return route.startsWith("anthropic/claude-");
84
84
  }
85
85
 
86
- function validDateAlias(alias: string): boolean {
86
+ export function validDateAlias(alias: string): boolean {
87
87
  const match = DATE_ALIAS.exec(alias);
88
88
  if (!match) return false;
89
89
  const year = Number(match[1]!.slice(0, 4));
@@ -0,0 +1,185 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { readConfigDiagnostics } from "../config";
4
+ import { readServiceApiTokenState, readTokenBackupState, serviceApiTokenFingerprint } from "../lib/service-secrets";
5
+ import { profilePath, type Desktop3pMetadata } from "./desktop-3p-library";
6
+ import { StoreIO, metadata, storePaths, MAX_DESKTOP_METADATA_ENTRIES, type JsonFile } from "./desktop-remote-store-io";
7
+ import {
8
+ DesktopStoreError, digest, origin, parseBaseline, parseDisconnect, parseOwner, parseState, projection, projectionHash, mergeProjection,
9
+ sameOwner, type ArtifactPending, type Baseline, type DesktopRemoteOwner, type Projection, type StoreState,
10
+ } from "./desktop-remote-store-state";
11
+
12
+ export function currentConnection(owner: DesktopRemoteOwner) {
13
+ parseOwner(owner);
14
+ const { config, source } = readConfigDiagnostics();
15
+ const client = config.client;
16
+ if (source !== "file" || config.runtimeRole !== "client" || !client
17
+ || origin(client.serverUrl) !== owner.serverUrl || client.apiKeyId !== owner.apiKeyId || client.connectedAt !== owner.connectedAt) throw new DesktopStoreError("conflict");
18
+ return { config, client };
19
+ }
20
+ export function serviceGenerations(): string[] {
21
+ const token = readServiceApiTokenState();
22
+ const backup = readTokenBackupState();
23
+ if (token.kind === "unsafe" || backup.kind === "unsafe") throw new DesktopStoreError("unsafe");
24
+ return [token, backup].flatMap(t => t.kind === "present" ? [t.fingerprint] : []);
25
+ }
26
+ export function profileCredential(value: Record<string, unknown> | null): { origin: string; fingerprint: string } | null {
27
+ if (!value || value.inferenceProvider !== "gateway" || value.inferenceCredentialKind !== "static") return null;
28
+ if (typeof value.inferenceGatewayApiKey !== "string") throw new DesktopStoreError("unsafe");
29
+ return { origin: origin(value.inferenceGatewayBaseUrl), fingerprint: serviceApiTokenFingerprint(value.inferenceGatewayApiKey) };
30
+ }
31
+ export function loadBundle(io: StoreIO) {
32
+ const paths = storePaths();
33
+ const stateFile = io.read(paths.state, 64 * 1024, true);
34
+ const baselineFile = io.read(paths.baseline, 1024 * 1024, true);
35
+ const state = stateFile ? parseState(stateFile.value) : null;
36
+ const baseline = baselineFile ? parseBaseline(baselineFile.value) : null;
37
+ if (!state && baseline) throw new DesktopStoreError("unsafe");
38
+ if (state) {
39
+ if (state.home !== paths.home || state.library !== paths.library) throw new DesktopStoreError("unsafe");
40
+ if (!baseline && state.phase !== "prepared" && state.phase !== "cleaned") throw new DesktopStoreError("unsafe");
41
+ if (baseline && (baselineFile!.hash !== state.baselineHash || baseline.home !== paths.home
42
+ || baseline.library !== paths.library || baseline.targetId !== state.targetId || baseline.kind !== state.baselineKind
43
+ || !sameOwner(baseline.owner, state.owner))) throw new DesktopStoreError("unsafe");
44
+ }
45
+ return { paths, stateFile, baselineFile, state, baseline };
46
+ }
47
+ export type Bundle = ReturnType<typeof loadBundle>;
48
+ export function requireOwner(bundle: Bundle, owner: DesktopRemoteOwner): void {
49
+ if (bundle.state && !sameOwner(bundle.state.owner, owner)) throw new DesktopStoreError("conflict");
50
+ }
51
+ export function selectedFile(io: StoreIO, library: string, meta: Desktop3pMetadata): JsonFile | null {
52
+ if (!meta.appliedId) return null;
53
+ const file = io.read(profilePath(library, meta.appliedId));
54
+ if (!file) throw new DesktopStoreError("unsafe");
55
+ return file;
56
+ }
57
+ export function locateLegacy(io: StoreIO, owner: DesktopRemoteOwner, known: readonly string[]) {
58
+ const paths = storePaths();
59
+ const meta = metadata(io, paths.library);
60
+ selectedFile(io, paths.library, meta.value);
61
+ let found: { id: string; file: JsonFile } | null = null;
62
+ for (const entry of meta.value.entries) {
63
+ const file = io.read(profilePath(paths.library, entry.id));
64
+ if (!file) { if (entry.id === meta.value.appliedId) throw new DesktopStoreError("unsafe"); continue; }
65
+ const credential = profileCredential(file.value);
66
+ if (!credential || credential.origin !== owner.serverUrl) continue;
67
+ if (entry.name !== "opencodex" || !known.includes(credential.fingerprint) || found) throw new DesktopStoreError("conflict");
68
+ found = { id: entry.id, file };
69
+ }
70
+ return found;
71
+ }
72
+ export function saveState(io: StoreIO, bundle: Bundle, state: StoreState): void {
73
+ parseState(state);
74
+ io.write(bundle.paths.state, state, bundle.stateFile, 64 * 1024);
75
+ bundle.state = state;
76
+ bundle.stateFile = io.read(bundle.paths.state, 64 * 1024, true);
77
+ }
78
+ export function establish(io: StoreIO, owner: DesktopRemoteOwner, known: readonly string[], onlyLegacy: boolean): Bundle | null {
79
+ let bundle = loadBundle(io);
80
+ const terminalFile = io.read(bundle.paths.disconnect, 64 * 1024, true);
81
+ const terminal = terminalFile ? parseDisconnect(terminalFile.value) : null;
82
+ if (terminal?.phase === "complete" && !sameOwner(terminal.owner, owner)) {
83
+ currentConnection(owner);
84
+ if (bundle.baselineFile || (bundle.state && (bundle.state.phase !== "cleaned" || !sameOwner(bundle.state.owner, terminal.owner)))) throw new DesktopStoreError("conflict");
85
+ if (bundle.stateFile) io.remove(bundle.paths.state, bundle.stateFile);
86
+ io.remove(bundle.paths.disconnect, terminalFile!);
87
+ bundle = loadBundle(io);
88
+ }
89
+ requireOwner(bundle, owner);
90
+ if (bundle.state && bundle.baseline) return bundle;
91
+ if (bundle.state?.phase === "cleaned") throw new DesktopStoreError("conflict");
92
+ const meta = metadata(io, bundle.paths.library);
93
+ const selected = selectedFile(io, bundle.paths.library, meta.value);
94
+ const legacy = locateLegacy(io, owner, known);
95
+ if (onlyLegacy && !legacy && !bundle.state) return null;
96
+ const entry = meta.value.entries.find(e => e.id === meta.value.appliedId && e.name === "opencodex")
97
+ ?? meta.value.entries.find(e => e.name === "opencodex");
98
+ const targetId = bundle.state?.targetId ?? legacy?.id ?? entry?.id ?? randomUUID();
99
+ if (!meta.value.entries.some(e => e.id === targetId) && meta.value.entries.length >= MAX_DESKTOP_METADATA_ENTRIES) {
100
+ throw new DesktopStoreError("conflict");
101
+ }
102
+ const target = io.read(profilePath(bundle.paths.library, targetId));
103
+ const credential = profileCredential(target?.value ?? null);
104
+ if (credential && known.includes(credential.fingerprint) && credential.origin !== owner.serverUrl) throw new DesktopStoreError("conflict");
105
+ const fallback = credential?.origin === owner.serverUrl;
106
+ if (fallback && (!known.includes(credential.fingerprint) || (entry?.name !== "opencodex" && legacy?.id !== targetId))) throw new DesktopStoreError("conflict");
107
+ const priorSelection = meta.value.appliedId && selected && !(fallback && meta.value.appliedId === targetId)
108
+ ? { id: meta.value.appliedId, hash: selected.hash } : null;
109
+ const baseline: Baseline = {
110
+ version: 1, owner, home: bundle.paths.home, library: bundle.paths.library, targetId,
111
+ kind: fallback ? "standard_fallback" : "known", targetExisted: target !== null,
112
+ projection: fallback ? {} : projection(target?.value ?? {}), priorSelection,
113
+ };
114
+ const baselineHash = digest(JSON.stringify(baseline, null, 2) + "\n");
115
+ if (bundle.state && (bundle.state.phase !== "prepared" || bundle.state.baselineHash !== baselineHash
116
+ || bundle.state.lastProjectionHash !== projectionHash(target?.value ?? null))) throw new DesktopStoreError("conflict");
117
+ if (!bundle.state) saveState(io, bundle, {
118
+ version: 1, owner, home: bundle.paths.home, library: bundle.paths.library, targetId,
119
+ baselineRef: "baseline.json", baselineHash, baselineKind: baseline.kind, phase: "prepared",
120
+ lastProjectionHash: projectionHash(target?.value ?? null), tokenFingerprint: known[0]!,
121
+ });
122
+ io.write(bundle.paths.baseline, baseline, null);
123
+ bundle = loadBundle(io);
124
+ saveState(io, bundle, { ...bundle.state!, phase: "active" });
125
+ return bundle;
126
+ }
127
+
128
+ export function artifact(io: StoreIO, bundle: Bundle, next: Projection, kind: ArtifactPending["kind"], tokenFingerprint: string, desiredSelection?: string | null): void {
129
+ const state = bundle.state!;
130
+ const targetPath = profilePath(bundle.paths.library, state.targetId);
131
+ let current = io.read(targetPath);
132
+ const meta = metadata(io, bundle.paths.library);
133
+ const existing = meta.value.entries.find(e => e.id === state.targetId);
134
+ // An absent row is valid only while a recorded first creation is unfinished.
135
+ // Keeping the original null projection receipt across apply -> restore proves it.
136
+ const uncommittedCreation = bundle.baseline?.targetExisted === false && state.lastProjectionHash === projectionHash(null);
137
+ if ((existing && existing.name !== "opencodex") || (!existing && !uncommittedCreation)) throw new DesktopStoreError("conflict");
138
+ if (!existing && meta.value.entries.length >= MAX_DESKTOP_METADATA_ENTRIES) throw new DesktopStoreError("conflict");
139
+ const before = projectionHash(current?.value ?? null), after = projectionHash(next);
140
+ const selection = meta.value.appliedId ?? null;
141
+ const pending = state.pending;
142
+ if (pending) {
143
+ if (pending.kind !== kind || pending.after !== after || pending.tokenFingerprint !== tokenFingerprint
144
+ || (before !== pending.before && before !== pending.after)) throw new DesktopStoreError("conflict");
145
+ if (kind === "apply" && selection !== pending.beforeSelection && selection !== pending.afterSelection) throw new DesktopStoreError("conflict");
146
+ } else if (before !== state.lastProjectionHash) throw new DesktopStoreError("conflict");
147
+ const afterSelection = desiredSelection === undefined ? selection : desiredSelection;
148
+ const settledPhase = kind === "restore" ? "restored" : "active";
149
+ if (!pending && existing && before === after && afterSelection === selection
150
+ && state.phase === settledPhase && state.tokenFingerprint === tokenFingerprint) return;
151
+ if (!pending) saveState(io, bundle, { ...state, pending: { kind, before, after, beforeSelection: selection, afterSelection, tokenFingerprint } });
152
+ if (before !== after) {
153
+ io.write(targetPath, mergeProjection(current?.value ?? {}, next), current);
154
+ current = io.read(targetPath);
155
+ }
156
+ // Build from fresh metadata, so unrelated rows/fields are never restored from baseline bytes.
157
+ const fresh = metadata(io, bundle.paths.library);
158
+ const freshEntry = fresh.value.entries.find(e => e.id === state.targetId);
159
+ if ((fresh.value.appliedId ?? null) !== selection || freshEntry?.name !== existing?.name) throw new DesktopStoreError("conflict");
160
+ if (!existing || afterSelection !== selection) {
161
+ if (!existing && fresh.value.entries.length >= MAX_DESKTOP_METADATA_ENTRIES) throw new DesktopStoreError("conflict");
162
+ if (afterSelection && !io.read(profilePath(bundle.paths.library, afterSelection))) throw new DesktopStoreError("unsafe");
163
+ const entries = existing ? fresh.value.entries : [...fresh.value.entries, { id: state.targetId, name: "opencodex" }];
164
+ const value = { ...fresh.value, entries, ...(afterSelection ? { appliedId: afterSelection } : {}) };
165
+ io.write(join(bundle.paths.library, "_meta.json"), value, fresh.file);
166
+ }
167
+ const { pending: _pending, ...settled } = bundle.state!;
168
+ saveState(io, bundle, { ...settled, phase: kind === "restore" ? "restored" : "active", lastProjectionHash: after, tokenFingerprint });
169
+ }
170
+ export function cleanBackup(io: StoreIO, bundle: Bundle, known: readonly string[]): void {
171
+ const path = `${profilePath(bundle.paths.library, bundle.state!.targetId)}.bak`;
172
+ const file = io.read(path);
173
+ if (!file) return;
174
+ const credential = profileCredential(file.value);
175
+ if (!credential || credential.origin !== bundle.state!.owner.serverUrl) return;
176
+ if (!known.includes(credential.fingerprint)) throw new DesktopStoreError("conflict");
177
+ const foreign = mergeProjection(file.value, {});
178
+ try {
179
+ if (Object.keys(foreign).length) io.write(path, foreign, file);
180
+ else io.remove(path, file);
181
+ } catch (error) {
182
+ if (error instanceof DesktopStoreError) throw error;
183
+ throw new DesktopStoreError("cleanup_pending");
184
+ }
185
+ }