@bitkyc08/opencodex 2.66.0 → 2.67.0-preview.20260926

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 (60) hide show
  1. package/bin/ocx.mjs +24 -11
  2. package/gui/dist/assets/{App-DyHlFeNB.js → App-B3VRRYSj.js} +15 -15
  3. package/gui/dist/assets/{App-gkoJIEOV.css → App-BJsT8Icc.css} +1 -1
  4. package/gui/dist/assets/{Tray-CI8cVKWu.js → Tray-TYAA0EQY.js} +1 -1
  5. package/gui/dist/assets/index-9Ju7nHsu.js +86 -0
  6. package/gui/dist/assets/index-DtNmX7hW.css +1 -0
  7. package/gui/dist/assets/{tray-data-_WyYzbMt.js → tray-data-xKfS2gSG.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/chat/inbound.ts +82 -3
  11. package/src/chat/outbound.ts +5 -3
  12. package/src/claude/desktop-first-party.ts +12 -3
  13. package/src/claude/first-party-settings.ts +97 -0
  14. package/src/claude/intercept/client-class.ts +18 -0
  15. package/src/claude/intercept/listener.ts +5 -0
  16. package/src/claude/intercept/runtime.ts +6 -0
  17. package/src/cli/capabilities.ts +12 -0
  18. package/src/cli/claude-desktop.ts +11 -6
  19. package/src/cli/claude.ts +25 -1
  20. package/src/cli/ensure-desired-integrations.ts +14 -1
  21. package/src/cli/integrations.ts +9 -1
  22. package/src/cli/uninstall-client-state.ts +6 -0
  23. package/src/cli/uninstall-integrations.ts +115 -0
  24. package/src/codex/history-job.ts +2 -1
  25. package/src/codex/main-account-hard-lock.ts +36 -19
  26. package/src/codex/quota.ts +11 -1
  27. package/src/combos/resolve.ts +11 -3
  28. package/src/config/load-degrade.ts +4 -0
  29. package/src/generated/compatibility-version.json +67 -43
  30. package/src/generated/worker-bundles.gen.ts +4 -0
  31. package/src/images/loop.ts +10 -3
  32. package/src/integrations/aside-profile-context.ts +17 -0
  33. package/src/lab/fabric/executor.ts +25 -3
  34. package/src/lab/fabric/producer-isolate.ts +199 -32
  35. package/src/lab/fabric/scratch.ts +9 -2
  36. package/src/lib/shadow-call.ts +11 -4
  37. package/src/lib/worker-embed.ts +33 -0
  38. package/src/oauth/generic-account-failover.ts +5 -3
  39. package/src/oauth/index.ts +19 -29
  40. package/src/oauth/store.ts +106 -34
  41. package/src/remote-control/workspace-rpc.ts +114 -19
  42. package/src/router.ts +7 -1
  43. package/src/server/chat-completions.ts +2 -1
  44. package/src/server/index/claude-intercept-lifecycle.ts +13 -0
  45. package/src/server/management/agent-settings-routes.ts +123 -5
  46. package/src/server/management/context.ts +4 -0
  47. package/src/server/management/native-integration-routes.ts +19 -8
  48. package/src/server/responses/core-normalize.ts +13 -1
  49. package/src/server/responses/request-prepare.ts +5 -3
  50. package/src/server/responses/sidecar-execution.ts +9 -2
  51. package/src/server/search.ts +28 -7
  52. package/src/storage/policy-job.ts +2 -1
  53. package/src/storage/restore-job.ts +2 -1
  54. package/src/types/config.ts +11 -5
  55. package/src/update/transactional-install.mjs +5 -1
  56. package/src/web-search/alpha-search.ts +71 -0
  57. package/src/web-search/devin-executor.ts +192 -0
  58. package/src/web-search/loop.ts +10 -3
  59. package/gui/dist/assets/index-BiwnVAUm.js +0 -86
  60. package/gui/dist/assets/index-ComVsdSk.css +0 -1
@@ -1226,7 +1226,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1226
1226
  const committed = persistCommittedDesktopGateway(config, state.profile, result.fingerprint);
1227
1227
  const modeWarning = committed.ok ? undefined : `Gateway applied, but its mode/profile state was not saved (${committed.reason}).`;
1228
1228
  // The durable marker describes the committed gateway even if old-mode cleanup fails.
1229
- const firstPartyRemoved = removeDesktopFirstParty();
1229
+ const firstPartyRemoved = removeDesktopFirstParty(loadConfig());
1230
1230
  if (!firstPartyRemoved.ok) {
1231
1231
  return { response: jsonResponse({
1232
1232
  error: `Claude Code settings could not be parsed (${firstPartyRemoved.path}); first-party cleanup remains incomplete after gateway apply.`,
@@ -1235,10 +1235,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1235
1235
  ...(modeWarning ? { warning: modeWarning } : {}),
1236
1236
  }, 500) };
1237
1237
  }
1238
- return { result, committed, modeWarning, pickerOff };
1238
+ return { result, committed, modeWarning, pickerOff, firstPartyRemoved };
1239
1239
  });
1240
1240
  if (outcome.response) return outcome.response;
1241
- const { result, committed, modeWarning, pickerOff } = outcome as Exclude<typeof outcome, { response: Response }>;
1241
+ const { result, committed, modeWarning, pickerOff, firstPartyRemoved } = outcome as Exclude<typeof outcome, { response: Response }>;
1242
1242
  const { claudeDesktopPolicyWarning, getCachedClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
1243
1243
  const policyState = deps.probeClaudeDesktopPolicy
1244
1244
  ? await deps.probeClaudeDesktopPolicy({ platform: deps.platform ?? process.platform })
@@ -1247,7 +1247,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1247
1247
  const pickerWarning = pickerOff.residual?.length
1248
1248
  ? `Picker mode cleanup is incomplete (${pickerOff.residual.join(", ")}); run ocx claude desktop picker off.`
1249
1249
  : undefined;
1250
- const warning = [modeWarning, policyWarning, pickerWarning].filter(Boolean).join(" ");
1250
+ const warning = [modeWarning, policyWarning, pickerWarning,
1251
+ firstPartyRemoved.retainedFor === "cli"
1252
+ ? "Shared first-party settings remain for Claude Code CLI." : undefined].filter(Boolean).join(" ");
1251
1253
  return jsonResponse({
1252
1254
  ok: true,
1253
1255
  mode: "gateway",
@@ -1447,8 +1449,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1447
1449
  );
1448
1450
  const webSearchOverride = config.claudeCode?.webSearchSidecar;
1449
1451
  const visionOverride = config.claudeCode?.visionSidecar;
1452
+ const { firstPartyDesired, readFirstPartyProxyStatus } = await import("../../claude/first-party-settings");
1453
+ const { observeClaudeDesktopMode } = await import("../../claude/desktop-first-party");
1454
+ const { claudeInterceptEnabled, getClaudeInterceptState } = await import("../../claude/intercept/runtime");
1455
+ const desired = firstPartyDesired(config, observeClaudeDesktopMode(config));
1456
+ const bound = (deps.getClaudeInterceptState ?? getClaudeInterceptState)();
1457
+ const eligible = claudeInterceptEnabled(config);
1458
+ const sharedProxy = readFirstPartyProxyStatus(config, bound?.proxyPort ?? null);
1450
1459
  return jsonResponse({
1451
1460
  enabled: config.claudeCode?.enabled !== false,
1461
+ cliFirstParty: config.claudeCode?.cliFirstParty === true,
1462
+ cliFirstPartyApplied: config.claudeCode?.cliFirstParty === true && sharedProxy === "live",
1463
+ desktopFirstParty: desired.desktop,
1464
+ interceptEligible: eligible,
1465
+ interceptRunning: bound !== null && eligible,
1466
+ sharedProxy,
1452
1467
  // Three-state intent (devlog 260726_claude_auth_auto): an absent key is AUTO, not
1453
1468
  // subscription. The old coercion made every save convert an untouched auto config
1454
1469
  // into a sticky manual subscription with no way back.
@@ -1505,7 +1520,110 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1505
1520
  return prototype === Object.prototype || prototype === null;
1506
1521
  };
1507
1522
  if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
1508
- const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
1523
+ const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
1524
+ if (body.cliFirstParty !== undefined && typeof body.cliFirstParty !== "boolean")
1525
+ return jsonResponse({ error: "cliFirstParty must be a boolean" }, 400);
1526
+ if (body.cliFirstParty !== undefined && Object.keys(body).length !== 1)
1527
+ return jsonResponse({ error: "cliFirstParty must be sent alone", code: "cli_first_party_not_alone" }, 400);
1528
+ if (body.cliFirstParty !== undefined) {
1529
+ const { claudeInterceptEnabled, claudeInterceptProxyPort, getClaudeInterceptState } = await import("../../claude/intercept/runtime");
1530
+ const { captureDesktopFirstPartyRollback, inspectDesktopFirstParty, observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
1531
+ const { firstPartyDesired, readFirstPartyProxyStatus, reconcileClaudeFirstPartySettings } = await import("../../claude/first-party-settings");
1532
+ const { commitClaudeCodeBlock } = await import("../../claude/claude-code-block");
1533
+ const bound = (deps.getClaudeInterceptState ?? getClaudeInterceptState)();
1534
+ if (body.cliFirstParty) {
1535
+ if (!claudeInterceptEnabled(config)) return jsonResponse({ error: "Claude intercept is disabled", code: "intercept_disabled" }, 409);
1536
+ if (bound === null) return jsonResponse({ error: "Claude intercept is unavailable", code: "intercept_unavailable" }, 409);
1537
+ const targetPort = claudeInterceptProxyPort(config, config.port ?? 10100);
1538
+ if (targetPort !== bound.proxyPort) return jsonResponse({
1539
+ error: `Claude intercept port mismatch (configured ${targetPort}, bound ${bound.proxyPort}); restart needed`,
1540
+ code: "intercept_unavailable",
1541
+ }, 409);
1542
+ let inspection: ReturnType<typeof inspectDesktopFirstParty>["settings"];
1543
+ try { inspection = inspectDesktopFirstParty(config).settings; }
1544
+ catch { return jsonResponse({ error: "Claude settings are unreadable", code: "unreadable" }, 500); }
1545
+ if (inspection.kind === "foreign") return jsonResponse({ error: "Claude settings env is foreign", code: "foreign_env" }, 409);
1546
+ if (inspection.kind === "unreadable") return jsonResponse({ error: "Claude settings are unreadable", code: "unreadable" }, 500);
1547
+ }
1548
+ let restoreSettings: (() => boolean) | undefined;
1549
+ try { if (body.cliFirstParty) restoreSettings = captureDesktopFirstPartyRollback(config); }
1550
+ catch { return jsonResponse({ error: "Claude settings are unreadable", code: "unreadable" }, 500); }
1551
+ type FirstPartyMutation =
1552
+ | { refusal: { error: string; code: "intercept_disabled" | "intercept_unavailable" } }
1553
+ | { claudeCode: OcxConfig["claudeCode"]; previous: { present: boolean; value: boolean }; pinnedMode: "first-party" | "gateway" | undefined };
1554
+ let outcome: ReturnType<typeof mutatePersistedConfig<FirstPartyMutation>>;
1555
+ try {
1556
+ outcome = mutatePersistedConfig<FirstPartyMutation>(persisted => {
1557
+ if (body.cliFirstParty) {
1558
+ if (!claudeInterceptEnabled(persisted)) return { changed: false, value: {
1559
+ refusal: { error: "Claude intercept is disabled", code: "intercept_disabled" as const } } };
1560
+ const persistedPort = claudeInterceptProxyPort(persisted, persisted.port ?? 10100);
1561
+ if (bound === null || persistedPort !== bound.proxyPort) return { changed: false, value: {
1562
+ refusal: { error: `Claude intercept port mismatch (configured ${persistedPort}, bound ${bound?.proxyPort ?? "none"}); restart needed`,
1563
+ code: "intercept_unavailable" as const } } };
1564
+ }
1565
+ const before = structuredClone(persisted);
1566
+ const previous = { present: Object.hasOwn(persisted.claudeCode ?? {}, "cliFirstParty"),
1567
+ value: persisted.claudeCode?.cliFirstParty === true };
1568
+ const pinnedMode = body.cliFirstParty && persisted.claudeCode?.desktopMode === undefined
1569
+ ? resolveClaudeDesktopMode(before, observeClaudeDesktopMode(before)) : undefined;
1570
+ const nextBlock = { ...(persisted.claudeCode ?? {}) };
1571
+ if (body.cliFirstParty) nextBlock.cliFirstParty = true;
1572
+ else delete nextBlock.cliFirstParty;
1573
+ if (pinnedMode) nextBlock.desktopMode = pinnedMode;
1574
+ commitClaudeCodeBlock(persisted, nextBlock);
1575
+ return { changed: true, value: { claudeCode: structuredClone(persisted.claudeCode), previous, pinnedMode } };
1576
+ });
1577
+ } catch { return jsonResponse({ error: "Could not save Claude settings", code: "write_failed" }, 500); }
1578
+ if (outcome.status === "unavailable") return jsonResponse({ error: "Could not save Claude settings", code: "write_failed" }, 500);
1579
+ const committed = outcome.value;
1580
+ if ("refusal" in committed) return jsonResponse(committed.refusal, 409);
1581
+ adoptPersistedClaudeCode(config, committed.claudeCode);
1582
+ const live = { ...(config.claudeCode ?? {}) };
1583
+ if (body.cliFirstParty) live.cliFirstParty = true; else delete live.cliFirstParty;
1584
+ if (committed.pinnedMode) live.desktopMode = committed.pinnedMode;
1585
+ config.claudeCode = live;
1586
+ let result: ReturnType<typeof reconcileClaudeFirstPartySettings> | undefined;
1587
+ try { result = (deps.reconcileClaudeFirstPartySettings ?? reconcileClaudeFirstPartySettings)(config,
1588
+ firstPartyDesired(config, observeClaudeDesktopMode(config))); }
1589
+ catch { /* An unexpected settings failure is mapped after conditional rollback. */ }
1590
+ if (!result?.ok) {
1591
+ let settingsRestored = true;
1592
+ if (body.cliFirstParty) {
1593
+ try { settingsRestored = restoreSettings?.() ?? false; } catch { settingsRestored = false; }
1594
+ let rollback: ReturnType<typeof mutatePersistedConfig<OcxConfig["claudeCode"]>>;
1595
+ try {
1596
+ rollback = mutatePersistedConfig(persisted => {
1597
+ const block = { ...(persisted.claudeCode ?? {}) };
1598
+ if (block.cliFirstParty === true) {
1599
+ if (committed.previous.present) block.cliFirstParty = committed.previous.value;
1600
+ else delete block.cliFirstParty;
1601
+ }
1602
+ if (committed.pinnedMode && block.desktopMode === committed.pinnedMode)
1603
+ delete block.desktopMode;
1604
+ persisted.claudeCode = block;
1605
+ return { changed: true, value: structuredClone(block) };
1606
+ });
1607
+ } catch { return jsonResponse({ error: "Claude settings rollback failed", code: "write_failed",
1608
+ warnings: ["settings_rollback_incomplete"] }, 500); }
1609
+ if (rollback.status === "unavailable") return jsonResponse({ error: "Claude settings rollback failed", code: "write_failed",
1610
+ warnings: ["settings_rollback_incomplete"] }, 500);
1611
+ adoptPersistedClaudeCode(config, rollback.value);
1612
+ }
1613
+ const code = result?.reason ?? "write_failed";
1614
+ return jsonResponse({ error: "Claude first-party reconciliation failed", code,
1615
+ ...(!body.cliFirstParty && code === "unreadable"
1616
+ ? { cliFirstParty: false, warnings: ["settings_residual"] } : {}),
1617
+ ...(body.cliFirstParty && !settingsRestored
1618
+ ? { warnings: ["settings_rollback_incomplete"] } : {}) },
1619
+ code === "intercept_disabled" || code === "foreign_env" ? 409 : 500);
1620
+ }
1621
+ const finalDesired = firstPartyDesired(config, observeClaudeDesktopMode(config));
1622
+ const residual = !finalDesired.desktop && !finalDesired.cli
1623
+ && readFirstPartyProxyStatus(config, bound?.proxyPort ?? null) !== "none";
1624
+ return jsonResponse({ ok: true, enabled: config.claudeCode?.enabled !== false,
1625
+ cliFirstParty: body.cliFirstParty, warnings: residual ? ["settings_residual"] : [] });
1626
+ }
1509
1627
  for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
1510
1628
  const section = body[field];
1511
1629
  if (section === undefined || section === null) continue;
@@ -44,6 +44,10 @@ export interface ManagementRequestIngress {
44
44
  }
45
45
 
46
46
  export interface ManagementApiDeps {
47
+ /** Bound Claude intercept state, injectable for isolated management-route tests. */
48
+ getClaudeInterceptState?: typeof import("../../claude/intercept/runtime").getClaudeInterceptState;
49
+ /** Reconciliation seam for field-scoped rollback tests. */
50
+ reconcileClaudeFirstPartySettings?: typeof import("../../claude/first-party-settings").reconcileClaudeFirstPartySettings;
47
51
  /** Read-only process-local aggregate metrics; absent keeps the scrape route unavailable. */
48
52
  requestMetrics?: RequestMetricsSnapshotter;
49
53
  checkPackageUpdate?: (channel: Channel) => Promise<UpdateCheckResult>;
@@ -21,7 +21,7 @@ import { commitClaudeCodeBlock } from "../../claude/claude-code-block";
21
21
  */
22
22
  import { join } from "node:path";
23
23
  import { existsSync } from "node:fs";
24
- import { getConfigPath, loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../../config";
24
+ import { adoptPersistedClaudeCode, getConfigPath, loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../../config";
25
25
  import { readRuntimePort } from "../../config/process-state";
26
26
  import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits } from "../../codex/catalog";
27
27
  import { getCodexHome } from "../../codex/paths";
@@ -676,9 +676,16 @@ function pickerCleanupNote(picker: DesktopPickerStatus): string {
676
676
  }
677
677
 
678
678
  /** Record which Desktop mode is applied; `false` when the config file could not be updated. */
679
- function persistDesktopModeMarker(desktopMode: ClaudeDesktopMode): boolean {
680
- const outcome = mutatePersistedConfig(persisted => recordClaudeDesktopMode(persisted, desktopMode));
681
- return outcome.status !== "unavailable";
679
+ function persistDesktopModeMarker(config: ManagementContext["config"], desktopMode: ClaudeDesktopMode): boolean {
680
+ const outcome = mutatePersistedConfig(persisted => {
681
+ const result = recordClaudeDesktopMode(persisted, desktopMode);
682
+ return { changed: result.changed, value: structuredClone(persisted.claudeCode) };
683
+ });
684
+ if (outcome.status === "unavailable") return false;
685
+ adoptPersistedClaudeCode(config, outcome.value);
686
+ // Pin the committed leaf: an unarmed baseline can otherwise retain a stale live mode.
687
+ recordClaudeDesktopMode(config, desktopMode);
688
+ return true;
682
689
  }
683
690
 
684
691
  let claudeDesktopToggleFlight: Promise<Response> | null = null;
@@ -705,13 +712,15 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
705
712
  }
706
713
  const desiredEnabled = loadConfig().clientIntegrations?.["claude-desktop"] !== false;
707
714
  const current = loadConfig();
715
+ // Publish committed Desktop intent to the live config used by intercept routing.
716
+ ctx.config.clientIntegrations = structuredClone(current.clientIntegrations);
708
717
  const fingerprint = current.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
709
718
 
710
719
  if (!body.enabled) {
711
720
  // Picker mode goes first: stop terminating claude.ai, drop its profile and trust.
712
721
  return await runPickerTransition(current, async ops => {
713
722
  const pickerOff = await ops.disableLocked({ persist: false });
714
- const firstPartyRemoved = removeDesktopFirstParty();
723
+ const firstPartyRemoved = removeDesktopFirstParty(loadConfig());
715
724
  if (!firstPartyRemoved.ok) {
716
725
  return postCommitRefusal(409, "claude-desktop", "write_failed",
717
726
  `Claude Code settings could not be read (${firstPartyRemoved.path}); the first-party proxy env was left in place.`, { desiredEnabled });
@@ -731,6 +740,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
731
740
  ok: true, clientId: "claude-desktop", changed, state: "absent", desiredEnabled,
732
741
  message: [
733
742
  changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
743
+ firstPartyRemoved.retainedFor === "cli" ? "Shared first-party settings remain for Claude Code CLI." : "",
734
744
  pickerCleanupNote(pickerOff),
735
745
  ].filter(Boolean).join(" "),
736
746
  } satisfies NativeToggleEnvelope);
@@ -755,7 +765,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
755
765
  return postCommitRefusal(500, "claude-desktop", "write_failed",
756
766
  "Gateway cleanup and first-party settings rollback did not complete.", { desiredEnabled });
757
767
  }
758
- const partialModeWarning = !removed.ok && removed.changed && !persistDesktopModeMarker("first-party")
768
+ const partialModeWarning = !removed.ok && removed.changed && !persistDesktopModeMarker(ctx.config, "first-party")
759
769
  ? " First-party is active but its mode marker was not saved." : "";
760
770
  if (removed.kind === "cleanup_incomplete") {
761
771
  return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
@@ -768,7 +778,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
768
778
  }
769
779
  gatewayRemoved = removed.changed;
770
780
  }
771
- const modeSaved = persistDesktopModeMarker("first-party");
781
+ const modeSaved = persistDesktopModeMarker(ctx.config, "first-party");
772
782
  const changed = applied.changed || gatewayRemoved;
773
783
  // Picker mode is on by default in first-party; only a committed mode turns it on.
774
784
  const picker = modeSaved && pickerPreferenceOn(loadConfig())
@@ -816,13 +826,14 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
816
826
  if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
817
827
  const committed = persistCommittedDesktopGateway(ctx.config, latest.claudeCode?.desktopProfile, result.fingerprint);
818
828
  const stateWarning = committed.ok ? "" : " The committed gateway mode/profile state was not saved.";
819
- const removed = removeDesktopFirstParty();
829
+ const removed = removeDesktopFirstParty(loadConfig());
820
830
  if (!removed.ok) return postCommitRefusal(500, "claude-desktop", "write_failed", "Gateway applied, but first-party settings cleanup did not complete." + stateWarning, { desiredEnabled: latestDesiredEnabled });
821
831
  return jsonResponse({
822
832
  ok: true, clientId: "claude-desktop", changed: true, state: "current", desiredEnabled: latestDesiredEnabled,
823
833
  message: [
824
834
  "Claude Desktop integration enabled.",
825
835
  stateWarning,
836
+ removed.retainedFor === "cli" ? "Shared first-party settings remain for Claude Code CLI." : "",
826
837
  pickerCleanupNote(pickerOff),
827
838
  ].filter(Boolean).join(" "),
828
839
  } satisfies NativeToggleEnvelope);
@@ -12,7 +12,7 @@ import { subagentFallbackNeedsModelEntitlements } from "../../codex/subagent-mod
12
12
  import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
13
13
  import type { RequestLogContext } from "../request-log";
14
14
  import type { HandleResponsesOptions } from "./core-options";
15
- import { prepareEffortNormalization } from "../effort-policy";
15
+ import { prepareEffortNormalization, stripEmptyLadderEffort, supportedLadderFor } from "../effort-policy";
16
16
  import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
17
17
  import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
18
18
  import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
@@ -307,6 +307,18 @@ export async function applyFinalRouteRequestNormalization(args: {
307
307
  logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
308
308
  }
309
309
  }
310
+ // Chat ingress cannot strip effort against a provisional policy pick. Apply the
311
+ // concrete target's restriction to BOTH adapter options and the raw wire copy;
312
+ // policy-fallback retains the original body before this attempt-local mutation.
313
+ if (inboundWire === "chat" && supportedLadderFor(route)?.length === 0) {
314
+ parsed.options.reasoning = undefined;
315
+ const raw = parsed._rawBody as { reasoning?: unknown } | undefined;
316
+ if (raw) {
317
+ const reasoning = stripEmptyLadderEffort(raw.reasoning, []);
318
+ if (reasoning === undefined) delete raw.reasoning;
319
+ else raw.reasoning = reasoning;
320
+ }
321
+ }
310
322
  recordAttemptRequestedEffort(logCtx);
311
323
  logCtx.modelSupportsServiceTier = SERVICE_TIER_ADAPTERS.has(route.provider.adapter)
312
324
  ? modelServiceTierSupport
@@ -229,7 +229,10 @@ export async function prepareResponsesRequest(
229
229
  // here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
230
230
  // LOOKUP so the check can never observe a one-candidate collapse.
231
231
  let shadowCallIntercepted = false;
232
- if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
232
+ // A spawned sub-agent turn names its model on purpose; gpt-6-luna is both the helper
233
+ // slug and a default sub-agent model, so neither intercept site may rewrite that turn.
234
+ const threadSpawn = isThreadSpawnRequest(req.headers);
235
+ if (!options.comboAttempt && !options.compactionRoutingOverride && !threadSpawn && body && typeof body === "object" && !Array.isArray(body)) {
233
236
  const shadowIntercept = config.shadowCallIntercept;
234
237
  const rawShadowModel = (body as { model?: unknown }).model;
235
238
  if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
@@ -483,7 +486,7 @@ export async function prepareResponsesRequest(
483
486
  : routeModel(config, modelId, evidenceFromBody(parsed._rawBody)));
484
487
  const _sci = config.shadowCallIntercept;
485
488
  let shadowRoute: RouteResult | undefined;
486
- if (!options.compactionRoutingOverride && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
489
+ if (!options.compactionRoutingOverride && !threadSpawn && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
487
490
  const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
488
491
  let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
489
492
  try {
@@ -549,7 +552,6 @@ export async function prepareResponsesRequest(
549
552
  // Exact account selectors are isolated from Pool-wide quota work. A canonical replay miss must
550
553
  // also fail closed without polling quota upstream. Cached fallback state can still select a
551
554
  // provider with native continuation support below.
552
- const threadSpawn = isThreadSpawnRequest(req.headers);
553
555
  const initialSubagentFallbackChain = threadSpawn && !options.comboAttempt
554
556
  ? resolveSubagentFallbackChain(parsed, config)
555
557
  : null;
@@ -160,7 +160,12 @@ export async function executeResponsesSidecars(
160
160
  retryAfter: string | null,
161
161
  responseHeaders?: Headers,
162
162
  retryParsed?: OcxParsedRequest,
163
- ): Promise<ProviderAdapter | null> => {
163
+ ): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
164
+ // Which credential axis actually moved. The main routed path already reports these three
165
+ // separately (`adapter-dispatch`: key-429 / anthropic-oauth-429 / oauth-account-429); the
166
+ // sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
167
+ // rotation in the attempt row and in the Logs UI.
168
+ let recoveryKind: AttemptRecoveryKind = "key-429";
164
169
  const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
165
170
  retryAfter,
166
171
  now: Date.now(),
@@ -209,6 +214,7 @@ export async function executeResponsesSidecars(
209
214
  hop.permit?.release();
210
215
  return null;
211
216
  }
217
+ recoveryKind = "oauth-account-429";
212
218
  hop.permit?.use();
213
219
  } else if (
214
220
  // Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
@@ -252,6 +258,7 @@ export async function executeResponsesSidecars(
252
258
  hop.permit?.release();
253
259
  return null;
254
260
  }
261
+ recoveryKind = "anthropic-oauth-429";
255
262
  hop.permit?.use();
256
263
  } else {
257
264
  // No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
@@ -281,7 +288,7 @@ export async function executeResponsesSidecars(
281
288
  provider: route.provider,
282
289
  adapterName: rotatedAdapter.name,
283
290
  });
284
- return rotatedAdapter;
291
+ return { adapter: rotatedAdapter, recoveryKind };
285
292
  };
286
293
  if ((imgPlan || vidPlan) && wsPlan) {
287
294
  // Web search takes priority when both are active — the media bridge cannot run
@@ -38,8 +38,8 @@ import {
38
38
  resolveFirstUsableOpenAiSidecar,
39
39
  type ExactOpenAiSidecarAccount,
40
40
  } from "../providers/openai-sidecar";
41
- import { routeModel } from "../router";
42
- import { handleAlphaSearchSidecarFallback } from "../web-search/alpha-search";
41
+ import { previewRouteModel, routeModel } from "../router";
42
+ import { handleAlphaSearchSidecarFallback, handleDevinAlphaSearch } from "../web-search/alpha-search";
43
43
  import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
44
44
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
45
45
  import type { RequestLogContext } from "./request-log";
@@ -65,11 +65,6 @@ export async function handleSearch(
65
65
  turnAdmissionLease?: AdmissionLease,
66
66
  admission?: DataPlaneAdmission,
67
67
  ): Promise<Response> {
68
- try { validateForwardAdmissionCredential(req.headers, config); }
69
- catch (err) {
70
- if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
71
- throw err;
72
- }
73
68
  let body: unknown;
74
69
  try {
75
70
  body = await readJsonRequestBody(req, undefined, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
@@ -79,6 +74,32 @@ export async function handleSearch(
79
74
  const model = (body as { model?: unknown } | null)?.model;
80
75
  if (typeof model === "string" && model) logCtx.model = model;
81
76
 
77
+ if (typeof model === "string" && model.trim()) {
78
+ try {
79
+ const route = previewRouteModel(config, model);
80
+ if (route.providerName === "devin" || route.staticPolicy.model.adapter === "devin") {
81
+ const denial = admissionScopeDenial(config, admission, model, route);
82
+ if (denial) return denial;
83
+ logCtx.provider = route.providerName;
84
+ logCtx.routeDecision = route.routeDecision;
85
+ return handleDevinAlphaSearch(
86
+ body,
87
+ "devin",
88
+ config.search?.timeoutMs ?? SEARCH_UPSTREAM_TIMEOUT_MS,
89
+ req.signal,
90
+ );
91
+ }
92
+ } catch {
93
+ // Preview is advisory: existing relay/fallback still owns unsupported or unroutable models.
94
+ }
95
+ }
96
+
97
+ try { validateForwardAdmissionCredential(req.headers, config); }
98
+ catch (err) {
99
+ if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
100
+ throw err;
101
+ }
102
+
82
103
  let exactAccount: ExactOpenAiSidecarAccount | undefined;
83
104
  let relayBody = body;
84
105
  const accountNamespace = typeof model === "string"
@@ -6,6 +6,7 @@
6
6
  * proxy event loop stays responsive.
7
7
  */
8
8
  import type { CleanupMode, CleanupResult } from "./cleanup";
9
+ import { spawnWorker } from "../lib/worker-embed";
9
10
  import { resolveCodexHomeDir } from "../codex/home";
10
11
  import {
11
12
  tryBeginStorageMutation,
@@ -310,7 +311,7 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
310
311
  let settled = false;
311
312
  let worker: Worker;
312
313
  try {
313
- worker = new Worker(new URL("./policy-worker.ts", import.meta.url).href);
314
+ worker = spawnWorker(new URL("./policy-worker.ts", import.meta.url).href, "policy-worker");
314
315
  reservation.bind(worker);
315
316
  } catch (error) {
316
317
  reservation.release();
@@ -9,6 +9,7 @@
9
9
  * `storage_mutation_busy` (409) instead of queueing.
10
10
  */
11
11
  import { resolveCodexHomeDir } from "../codex/home";
12
+ import { spawnWorker } from "../lib/worker-embed";
12
13
  import { restoreTrashEntry, type RestoreResult, type RestoreTestHooks } from "./cleanup";
13
14
  import {
14
15
  resetStorageMutationCoordinatorForTests,
@@ -167,7 +168,7 @@ function runInWorker(opts: {
167
168
  let settled = false;
168
169
  let worker: Worker;
169
170
  try {
170
- worker = new Worker(new URL("./restore-worker.ts", import.meta.url).href);
171
+ worker = spawnWorker(new URL("./restore-worker.ts", import.meta.url).href, "restore-worker");
171
172
  reservation.bind(worker);
172
173
  } catch (error) {
173
174
  reservation.release();
@@ -37,6 +37,9 @@ export interface OcxProtocolsConfig {
37
37
  * /v1/messages surface, the `ocx claude` launcher, and the GUI Claude page.
38
38
  */
39
39
  export interface OcxClaudeCodeConfig {
40
+ /** Route the standalone Claude Code CLI through the first-party intercept (settings.json env).
41
+ * Independent of Desktop's first-party mode. Absent/false = off. */
42
+ cliFirstParty?: boolean;
40
43
  /**
41
44
  * Opt-in relocation of supported trailing Claude harness notices from system instructions
42
45
  * to a user input message on translated routes. Changes the Desktop cache-key prefix.
@@ -737,19 +740,22 @@ export interface OcxConfig {
737
740
  /**
738
741
  * Shadow call intercept: redirect Codex's hard-coded helper calls (title generation,
739
742
  * commit messages, skill orchestration) to a user-chosen model. Default intercepted
740
- * source model: gpt-5.6-luna (Codex 0.145.0+). Clients through 0.144.x emitted
741
- * gpt-5.4-mini instead; that model is retired upstream, but it stays available as an
742
- * opt-in `sourceModels` prefix so an old client's helper calls can still be intercepted.
743
+ * source models: gpt-6-luna (Codex 0.154.0+) and gpt-5.6-luna (0.145.0-0.153.x).
744
+ * Clients through 0.144.x emitted gpt-5.4-mini instead; that model is retired upstream,
745
+ * but it stays available as an opt-in `sourceModels` prefix so an old client's helper
746
+ * calls can still be intercepted.
743
747
  * Opt-in; disabled by default. Matching requests preserve their configured reasoning effort.
744
748
  * All requests for configured shadow source models are intercepted regardless of request kind,
745
- * except when the replacement intersects the same provider+model source set.
749
+ * except when the replacement intersects the same provider+model source set, and except
750
+ * spawned sub-agent turns (x-openai-subagent: collab_spawn / subagent_kind thread_spawn),
751
+ * which keep the model they were spawned with.
746
752
  */
747
753
  shadowCallIntercept?: {
748
754
  /** When true, requests for known shadow/helper source models are rewritten to the configured model. */
749
755
  enabled?: boolean;
750
756
  /** Replacement model id (e.g. "gpt-5.5"). */
751
757
  model?: string;
752
- /** Optional override of intercepted source-model prefixes (default: gpt-5.6-luna). */
758
+ /** Optional override of intercepted source-model prefixes (default: gpt-6-luna, gpt-5.6-luna). */
753
759
  sourceModels?: string[];
754
760
  };
755
761
  /**
@@ -189,8 +189,12 @@ function createOwnedStage(scopeDir, pkgName, deps = {}) {
189
189
  pid: process.pid,
190
190
  createdAt: (deps.now ?? Date.now)(),
191
191
  }), { flag: "wx" });
192
+ // npm's strict script policy plans the global tree before it creates the prefix layout, so
193
+ // `-g --prefix` into a bare stage fails with ENOENT on <stage>/lib (#5760). POSIX global
194
+ // prefixes keep packages under lib/; Windows installs into the prefix itself.
195
+ if (process.platform !== "win32") mkdir(join(stageRoot, "lib"));
192
196
  } catch (error) {
193
- // Still empty and created by this call: remove it rather than leave an unmarked stage.
197
+ // Created by this call and not yet handed to npm: remove it rather than leave a partial stage.
194
198
  try { rmSync(stageRoot, { recursive: true, force: true }); } catch { /* reported by the caller */ }
195
199
  throw error;
196
200
  }
@@ -12,12 +12,14 @@
12
12
  * and pulling it in recreates the cycle sidecar-providers.ts exists to avoid.
13
13
  */
14
14
  import { formatErrorResponse } from "../bridge";
15
+ import { signalWithTimeout } from "../lib/abort";
15
16
  import { redactSecretString } from "../lib/redact";
16
17
  import { sidecarEnter } from "../lib/sidecar-tracker";
17
18
  import { admissionScopeDenial } from "../server/admission-model-scope";
18
19
  import type { DataPlaneAdmission } from "../server/auth-cors";
19
20
  import type { OcxConfig, OcxProviderConfig, OcxWebSearchSidecarConfig } from "../types";
20
21
  import { runAnthropicWebSearch } from "./anthropic-executor";
22
+ import { resolveDevinWebSearchSnapshot, runDevinWebSearch } from "./devin-executor";
21
23
  import { runExaWebSearch } from "./exa-executor";
22
24
  import type { SidecarOutcome, SidecarSettings } from "./executor";
23
25
  import { runGeminiWebSearch } from "./gemini-executor";
@@ -203,6 +205,75 @@ async function runAlphaSearchQuery(
203
205
  }
204
206
  }
205
207
 
208
+ function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
209
+ if (signal.aborted) return Promise.reject(signal.reason);
210
+ return new Promise<T>((resolve, reject) => {
211
+ const onAbort = () => reject(signal.reason);
212
+ signal.addEventListener("abort", onAbort, { once: true });
213
+ promise.then(
214
+ value => { signal.removeEventListener("abort", onAbort); resolve(value); },
215
+ error => { signal.removeEventListener("abort", onAbort); reject(error); },
216
+ );
217
+ });
218
+ }
219
+
220
+ export async function handleDevinAlphaSearch(
221
+ body: unknown,
222
+ providerName: string,
223
+ timeoutMs: number,
224
+ signal?: AbortSignal,
225
+ ): Promise<Response> {
226
+ const queries = extractAlphaSearchQueries(body);
227
+ if (queries.length === 0) {
228
+ return formatErrorResponse(
229
+ 400,
230
+ "invalid_request_error",
231
+ "Built-in web search request is missing a usable query (commands.search_query, query, q, or search_query).",
232
+ );
233
+ }
234
+ const deadline = signalWithTimeout(timeoutMs, signal);
235
+ try {
236
+ const resolved = await raceAbort(resolveDevinWebSearchSnapshot(providerName), deadline.signal);
237
+ if ("error" in resolved) {
238
+ return formatErrorResponse(502, "upstream_error", `devin web search failed: ${redactSecretString(resolved.error)}`);
239
+ }
240
+ const texts: string[] = [];
241
+ const sources: SidecarOutcome["sources"] = [];
242
+ for (const query of queries) {
243
+ const outcome = await runDevinWebSearch(query, resolved.snapshot, deadline.signal);
244
+ if (outcome.error) {
245
+ if (signal?.aborted) {
246
+ return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
247
+ }
248
+ if (deadline.signal.aborted) {
249
+ return formatErrorResponse(504, "upstream_error", "devin web search timed out");
250
+ }
251
+ const detail = redactSecretString(outcome.error);
252
+ return formatErrorResponse(502, "upstream_error", `devin web search failed: ${detail}`);
253
+ }
254
+ texts.push(queries.length > 1 ? `Results for "${query}":\n${outcome.text}` : outcome.text);
255
+ for (const source of outcome.sources) {
256
+ if (!sources.some(existing => existing.url === source.url)) sources.push(source);
257
+ }
258
+ }
259
+ return new Response(JSON.stringify(formatAlphaSearchBody(texts.join("\n\n"), sources)), {
260
+ status: 200,
261
+ headers: { "content-type": "application/json" },
262
+ });
263
+ } catch (error) {
264
+ if (signal?.aborted) {
265
+ return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
266
+ }
267
+ if (deadline.signal.aborted) {
268
+ return formatErrorResponse(504, "upstream_error", "devin web search timed out");
269
+ }
270
+ const detail = redactSecretString(error instanceof Error ? error.message : String(error));
271
+ return formatErrorResponse(502, "upstream_error", `devin web search failed: ${detail}`);
272
+ } finally {
273
+ deadline.cleanup();
274
+ }
275
+ }
276
+
206
277
  function formatAlphaSearchBody(text: string, sources: SidecarOutcome["sources"]): {
207
278
  encrypted_output: null;
208
279
  output: string;