@bitkyc08/opencodex 2.68.0 → 2.69.0-preview.20260928

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 (175) hide show
  1. package/bin/ocx.mjs +25 -17
  2. package/gui/dist/assets/App-cDPyCb_V.js +51 -0
  3. package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-H1IAB8mN.js} +1 -1
  4. package/gui/dist/assets/index-BF9QnKO_.css +1 -0
  5. package/gui/dist/assets/index-CI6YTp8z.js +86 -0
  6. package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-CutOlPu2.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/factory-droid.svg +8 -0
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-output-schema.ts +109 -11
  11. package/src/adapters/coding-agent/protocol.ts +106 -11
  12. package/src/adapters/coding-agent/turn.ts +23 -18
  13. package/src/adapters/google-errors.ts +16 -1
  14. package/src/adapters/google-http.ts +14 -0
  15. package/src/adapters/openai-chat/passthrough.ts +2 -0
  16. package/src/adapters/openai-chat/summary-budget.ts +72 -0
  17. package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
  18. package/src/adapters/openai-chat.ts +6 -9
  19. package/src/bridge/response-json.ts +3 -1
  20. package/src/bridge/sse.ts +3 -1
  21. package/src/claude/desktop-picker.ts +6 -2
  22. package/src/claude/desktop-system-proxy.ts +155 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound.ts +1 -1
  25. package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
  26. package/src/claude/intercept/picker-ca.ts +283 -83
  27. package/src/claude/intercept/picker-models.ts +4 -1
  28. package/src/claude/intercept/picker-trust.ts +15 -2
  29. package/src/claude/intercept/runtime.ts +99 -32
  30. package/src/cli/account-api.ts +5 -2
  31. package/src/cli/account-extended.ts +47 -11
  32. package/src/cli/account-target.ts +12 -7
  33. package/src/cli/account.ts +47 -3
  34. package/src/cli/capabilities.ts +33 -7
  35. package/src/cli/claude-agent-startup-sync.ts +2 -0
  36. package/src/cli/claude-desktop.ts +37 -13
  37. package/src/cli/cross-home-owner.ts +138 -0
  38. package/src/cli/dispatch.ts +1 -1
  39. package/src/cli/doctor.ts +18 -8
  40. package/src/cli/ensure-desired-integrations.ts +5 -0
  41. package/src/cli/export-command.ts +6 -6
  42. package/src/cli/help.ts +1 -1
  43. package/src/cli/index.ts +15 -18
  44. package/src/cli/integrations.ts +8 -1
  45. package/src/cli/models-runtime-subcommands.ts +1 -0
  46. package/src/cli/models-runtime.ts +106 -4
  47. package/src/cli/models.ts +6 -1
  48. package/src/cli/opencode.ts +2 -83
  49. package/src/cli/registry.ts +3 -2
  50. package/src/cli/status.ts +86 -7
  51. package/src/client/connect.ts +24 -10
  52. package/src/client/link-ingress.ts +3 -1
  53. package/src/client/link-join.ts +76 -9
  54. package/src/client/link-relay-transport.ts +184 -0
  55. package/src/client/link-relay.ts +16 -4
  56. package/src/client/machine-listener.ts +2 -0
  57. package/src/clients/config-export/constants.ts +8 -0
  58. package/src/clients/config-export/contracts.ts +13 -1
  59. package/src/clients/config-export/droid.ts +71 -0
  60. package/src/clients/config-export/kilo.ts +116 -0
  61. package/src/clients/config-export.ts +33 -2
  62. package/src/codex/auth-api/routes.ts +1 -1
  63. package/src/codex/catalog/access-programs.ts +14 -2
  64. package/src/codex/catalog/provider-models.ts +1 -1
  65. package/src/codex/catalog-auto-refresh.ts +122 -0
  66. package/src/codex/config-drift-heal.ts +80 -0
  67. package/src/codex/desktop-switches.ts +7 -5
  68. package/src/codex/inject/bounded-config-reader.ts +69 -0
  69. package/src/codex/inject/config-toml.ts +31 -3
  70. package/src/codex/inject/routing-target.ts +6 -1
  71. package/src/codex/inject.ts +4 -0
  72. package/src/codex/model-entitlements.ts +32 -5
  73. package/src/codex/native-profile-startup.ts +17 -5
  74. package/src/codex/project-config-warnings.ts +72 -21
  75. package/src/codex/quota-auto-refresh-state.ts +8 -2
  76. package/src/codex/quota-auto-refresh.ts +80 -9
  77. package/src/codex/sync.ts +10 -0
  78. package/src/combos/failover.ts +4 -1
  79. package/src/combos/jev.ts +15 -4
  80. package/src/combos/types.ts +16 -0
  81. package/src/config/diagnostics.ts +19 -1
  82. package/src/config/load-degrade.ts +29 -0
  83. package/src/config/macos-system-proxy.ts +114 -0
  84. package/src/config/proxy-env.ts +67 -20
  85. package/src/config/schema/config-schema.ts +15 -0
  86. package/src/config/schema/leaf-validators.ts +23 -0
  87. package/src/config.ts +2 -0
  88. package/src/generated/compatibility-version.json +220 -144
  89. package/src/integrations/catalog-refresh.ts +1 -1
  90. package/src/integrations/config-io.ts +12 -3
  91. package/src/integrations/droid-settings.ts +65 -0
  92. package/src/integrations/kilo-candidates.ts +30 -0
  93. package/src/integrations/mutation-plan.ts +62 -7
  94. package/src/integrations/native/ownership-preflight.ts +46 -12
  95. package/src/integrations/registry.ts +136 -7
  96. package/src/integrations/state.ts +52 -4
  97. package/src/integrations/target.ts +3 -0
  98. package/src/integrations/writer.ts +55 -14
  99. package/src/lib/account-selection-events.ts +14 -0
  100. package/src/lib/jsonc.ts +110 -0
  101. package/src/lib/standalone.ts +9 -1
  102. package/src/lib/windows-system-proxy.ts +101 -1
  103. package/src/link/relay-auth.ts +29 -0
  104. package/src/link/ssh-argv.ts +6 -3
  105. package/src/link/ssh-runner.ts +5 -5
  106. package/src/oauth/generic-account-failover.ts +52 -8
  107. package/src/oauth/index.ts +37 -14
  108. package/src/oauth/store.ts +96 -11
  109. package/src/oauth/token-guardian.ts +7 -3
  110. package/src/oauth/types.ts +2 -0
  111. package/src/plugins/loader.ts +5 -3
  112. package/src/providers/fastwire.ts +3 -0
  113. package/src/providers/key-failover.ts +13 -5
  114. package/src/providers/kiro-model-catalog.ts +4 -3
  115. package/src/providers/model-rename-fields.ts +1 -0
  116. package/src/providers/openai-tiers-destination.ts +9 -0
  117. package/src/providers/quota/account-cache.ts +17 -3
  118. package/src/providers/quota/vendor-probes-oauth.ts +2 -0
  119. package/src/providers/quota.ts +2 -2
  120. package/src/responses/code-mode-helper-compat.ts +14 -1
  121. package/src/responses/custom-tool-compat.ts +1 -1
  122. package/src/server/auth-cors.ts +1 -0
  123. package/src/server/images.ts +70 -20
  124. package/src/server/index/link-listener.ts +20 -3
  125. package/src/server/index/link-relay-sessions.ts +138 -0
  126. package/src/server/index/optional-listeners.ts +11 -1
  127. package/src/server/index/serve-options.ts +12 -0
  128. package/src/server/inference/client-encoder-delivery.ts +1 -0
  129. package/src/server/local-management-read-client.ts +21 -2
  130. package/src/server/management/agent-settings-routes.ts +19 -5
  131. package/src/server/management/combo-routes.ts +5 -3
  132. package/src/server/management/config-routes.ts +32 -6
  133. package/src/server/management/model-routes.ts +232 -7
  134. package/src/server/management/model-rows.ts +199 -1
  135. package/src/server/management/oauth-account-routes.ts +63 -3
  136. package/src/server/management/route-registry.ts +2 -0
  137. package/src/server/port-reclaim.ts +170 -32
  138. package/src/server/responses/adapter-delivery.ts +6 -4
  139. package/src/server/responses/adapter-dispatch.ts +72 -2
  140. package/src/server/responses/core-combo.ts +6 -1
  141. package/src/server/responses/core-normalize.ts +19 -4
  142. package/src/server/responses/core-options.ts +2 -0
  143. package/src/server/responses/memory-models.ts +169 -0
  144. package/src/server/responses/passthrough-delivery.ts +4 -0
  145. package/src/server/responses/passthrough-dispatch.ts +10 -0
  146. package/src/server/responses/request-prepare.ts +86 -4
  147. package/src/server/responses/request-transport.ts +4 -0
  148. package/src/server/responses/run-turn-execution.ts +5 -2
  149. package/src/server/responses/shadow-target-availability.ts +17 -4
  150. package/src/server/responses/sidecar-execution.ts +18 -2
  151. package/src/server/responses/skills-snapshot.ts +239 -0
  152. package/src/server/responses-undeclared-tool-guard.ts +43 -6
  153. package/src/server/search.ts +1 -1
  154. package/src/service/guards.ts +4 -5
  155. package/src/service/state.ts +39 -2
  156. package/src/service.ts +2 -2
  157. package/src/types/config.ts +46 -4
  158. package/src/types/provider.ts +9 -0
  159. package/src/types/request.ts +6 -0
  160. package/src/types/tools.ts +38 -8
  161. package/src/types.ts +3 -0
  162. package/src/update/async-check.ts +5 -1
  163. package/src/update/index.ts +30 -12
  164. package/src/update/job.ts +3 -1
  165. package/src/update/pnpm-read-policy.d.mts +4 -0
  166. package/src/update/pnpm-read-policy.mjs +44 -0
  167. package/src/update/worker-launch.ts +152 -11
  168. package/src/usage/cost.ts +6 -3
  169. package/src/usage/log.ts +4 -0
  170. package/src/web-search/backends.ts +2 -2
  171. package/src/web-search/devin-executor.ts +1 -1
  172. package/src/web-search/sidecar-providers.ts +5 -5
  173. package/gui/dist/assets/App-mpsibsFj.js +0 -51
  174. package/gui/dist/assets/index-BDUBS8PW.css +0 -1
  175. package/gui/dist/assets/index-kUE8tuAK.js +0 -86
@@ -226,6 +226,35 @@ export function collectDeclaredBareWireToolNames(body: unknown): Set<string> {
226
226
  return names;
227
227
  }
228
228
 
229
+ /** Custom declarations with no foreign namespace, for code-mode exec recovery. */
230
+ export function collectDeclaredBareCustomWireToolNames(body: unknown): Set<string> {
231
+ const names = new Set<string>();
232
+ if (!isPlainObject(body)) return names;
233
+ const specGroups: unknown[] = [body.tools];
234
+ if (Array.isArray(body.input)) {
235
+ for (const item of body.input) {
236
+ if (isPlainObject(item) && (item.type === "additional_tools" || item.type === "tool_search_output")) {
237
+ specGroups.push(item.tools);
238
+ }
239
+ }
240
+ }
241
+ const add = (tool: unknown): void => {
242
+ if (isPlainObject(tool) && tool.type === "custom" && typeof tool.name === "string") names.add(tool.name);
243
+ };
244
+ for (const specs of specGroups) {
245
+ if (!Array.isArray(specs)) continue;
246
+ for (const spec of specs) {
247
+ if (!isPlainObject(spec)) continue;
248
+ if (spec.type === "namespace") {
249
+ if (spec.name === BUILTIN_FUNCTIONS_NAMESPACE && Array.isArray(spec.tools)) {
250
+ for (const inner of spec.tools) add(inner);
251
+ }
252
+ } else add(spec);
253
+ }
254
+ }
255
+ return names;
256
+ }
257
+
229
258
  function addNamelessClientCallTypes(callTypes: Set<string>, specs: unknown): void {
230
259
  if (!Array.isArray(specs)) return;
231
260
  for (const spec of specs) {
@@ -349,6 +378,7 @@ export function hasExplicitWireToolCatalog(body: unknown): boolean {
349
378
  * @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
350
379
  * @param providerExecutedCallTypes - Call types executed by the provider.
351
380
  * @param declaredBare - Explicitly declared bare tool names without namespace provenance.
381
+ * @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
352
382
  * @returns The undeclared tool call name if unauthorized, or undefined if permitted.
353
383
  */
354
384
  function undeclaredNameInItem(
@@ -357,6 +387,7 @@ function undeclaredNameInItem(
357
387
  declaredNamelessClientCallTypes: ReadonlySet<string>,
358
388
  providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
359
389
  declaredBare?: ReadonlySet<string>,
390
+ declaredCustom?: ReadonlySet<string>,
360
391
  ): string | undefined {
361
392
  if (!isPlainObject(item)) return undefined;
362
393
  if (typeof item.type !== "string") return undefined;
@@ -396,7 +427,7 @@ function undeclaredNameInItem(
396
427
  ) return undefined;
397
428
  return name;
398
429
  }
399
- const effectiveName = normalizeDeclaredToolName(name, declared, declaredBare);
430
+ const effectiveName = normalizeDeclaredToolName(name, declared, declaredBare, declaredCustom);
400
431
  if (declared.has(effectiveName)) return undefined;
401
432
  return name;
402
433
  }
@@ -409,6 +440,7 @@ function undeclaredNameInItem(
409
440
  * @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
410
441
  * @param providerExecutedCallTypes - Call types executed by the provider.
411
442
  * @param declaredBare - Explicitly declared bare tool names without namespace provenance.
443
+ * @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
412
444
  * @returns The name of the first undeclared tool call, or undefined.
413
445
  */
414
446
  export function undeclaredToolCallName(
@@ -417,18 +449,19 @@ export function undeclaredToolCallName(
417
449
  declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
418
450
  providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
419
451
  declaredBare?: ReadonlySet<string>,
452
+ declaredCustom?: ReadonlySet<string>,
420
453
  ): string | undefined {
421
454
  if (!isPlainObject(payload)) return undefined;
422
455
  if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
423
- return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
456
+ return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
424
457
  }
425
458
  if (payload.type === "response.function_call_arguments.done" && typeof payload.name === "string") {
426
459
  const fakeItem = { type: "function_call", name: payload.name, namespace: payload.namespace };
427
- return undeclaredNameInItem(fakeItem, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
460
+ return undeclaredNameInItem(fakeItem, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
428
461
  }
429
462
  // Sparse gateways skip incremental items and only ever ship the terminal snapshot.
430
463
  if (payload.type === "response.completed" || payload.type === "response.incomplete") {
431
- return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
464
+ return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
432
465
  }
433
466
  return undefined;
434
467
  }
@@ -441,6 +474,7 @@ export function undeclaredToolCallName(
441
474
  * @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
442
475
  * @param providerExecutedCallTypes - Call types executed by the provider.
443
476
  * @param declaredBare - Explicitly declared bare tool names without namespace provenance.
477
+ * @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
444
478
  * @returns The name of the first undeclared tool call, or undefined.
445
479
  */
446
480
  export function undeclaredToolCallNameInResponse(
@@ -449,10 +483,11 @@ export function undeclaredToolCallNameInResponse(
449
483
  declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
450
484
  providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
451
485
  declaredBare?: ReadonlySet<string>,
486
+ declaredCustom?: ReadonlySet<string>,
452
487
  ): string | undefined {
453
488
  if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
454
489
  for (const item of response.output) {
455
- const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
490
+ const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
456
491
  if (name !== undefined) return name;
457
492
  }
458
493
  return undefined;
@@ -667,6 +702,7 @@ function failedBlocks(name: string, newline: string): readonly string[] {
667
702
  * @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
668
703
  * @param providerExecutedCallTypes - Call types executed by the provider.
669
704
  * @param declaredBare - Explicitly declared bare tool names without namespace provenance.
705
+ * @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
670
706
  * @returns An SSE block rewrite function.
671
707
  */
672
708
  export function createUndeclaredToolCallGuardBlockRewrite(
@@ -674,6 +710,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
674
710
  declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
675
711
  providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
676
712
  declaredBare?: ReadonlySet<string>,
713
+ declaredCustom?: ReadonlySet<string>,
677
714
  ): SseBlockRewrite {
678
715
  let tripped = false;
679
716
  return (block: string) => {
@@ -686,7 +723,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
686
723
  } catch {
687
724
  return [block];
688
725
  }
689
- const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
726
+ const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
690
727
  if (name !== undefined) {
691
728
  tripped = true;
692
729
  return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
@@ -84,7 +84,7 @@ export async function handleSearch(
84
84
  logCtx.routeDecision = route.routeDecision;
85
85
  return handleDevinAlphaSearch(
86
86
  body,
87
- "devin",
87
+ route.providerName,
88
88
  config.search?.timeoutMs ?? SEARCH_UPSTREAM_TIMEOUT_MS,
89
89
  req.signal,
90
90
  );
@@ -10,7 +10,7 @@ import { recordOwnedConfigPath } from "../lib/config-ownership";
10
10
  import { isTestHomeGuardArmed } from "../lib/test-home-guard";
11
11
  import { diagnoseService } from "./diagnostics";
12
12
  import type { ServiceDiagnostic } from "./diagnostics";
13
- import { currentCodexHome, currentOpenCodexHome, normalizePathForCompare, resolveServiceState, serviceCodexHomeMatchesInstall } from "./state";
13
+ import { currentCodexHome, currentOpenCodexHome, resolveServiceState, serviceCodexHomeMatchesInstall, servicePathMatchesInstall } from "./state";
14
14
  import { resolveCodexSqliteHome } from "../codex/paths";
15
15
  import type { CodexHomeDeps } from "../codex/home";
16
16
  import { isLoopbackHostname } from "../codex/loopback-target";
@@ -58,9 +58,8 @@ export function assertServiceEnvironmentMatchesInstall(deps: CodexHomeDeps = {})
58
58
  `Rerun with CODEX_HOME=${state.codexHome} so native Codex restore updates the recorded home.`,
59
59
  );
60
60
  }
61
- const expectedOpenCodexHome = normalizePathForCompare(state.opencodexHome);
62
- const actualOpenCodexHome = normalizePathForCompare(currentOpenCodexHome());
63
- if (expectedOpenCodexHome !== actualOpenCodexHome) {
61
+ const actualOpenCodexHome = currentOpenCodexHome();
62
+ if (!servicePathMatchesInstall(state.opencodexHome, actualOpenCodexHome, deps)) {
64
63
  throw new ServiceOwnershipError(
65
64
  `Service was installed with OPENCODEX_HOME=${state.opencodexHome}, but current OPENCODEX_HOME=${currentOpenCodexHome()}. ` +
66
65
  "Run the service command from the same OpenCodex home so service state and secrets match.",
@@ -68,7 +67,7 @@ export function assertServiceEnvironmentMatchesInstall(deps: CodexHomeDeps = {})
68
67
  }
69
68
  if (state.codexSqliteHome !== undefined) {
70
69
  const actualCodexSqliteHome = resolveCodexSqliteHome({ codexHome: actualCodexHome });
71
- if (normalizePathForCompare(state.codexSqliteHome) !== normalizePathForCompare(actualCodexSqliteHome)) {
70
+ if (!servicePathMatchesInstall(state.codexSqliteHome, actualCodexSqliteHome, deps)) {
72
71
  throw new ServiceOwnershipError(
73
72
  `Service was installed with Codex SQLite home=${state.codexSqliteHome}, but the current Codex SQLite home=${actualCodexSqliteHome}. ` +
74
73
  "Run the service command with the same sqlite_home configuration and CODEX_SQLITE_HOME so native Codex history restore updates the correct database.",
@@ -1,4 +1,4 @@
1
- import { accessSync, constants as fsConstants, existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { accessSync, constants as fsConstants, existsSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { delimiter, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
4
4
  import { expandUserPath, getConfigDir } from "../config";
@@ -857,8 +857,45 @@ export function serviceHomeMatches(a: string, b: string): boolean {
857
857
  return normalizePathForCompare(a) === normalizePathForCompare(b);
858
858
  }
859
859
 
860
+ export type ServicePathComparison = "same" | "different" | "unknown";
861
+
862
+ /**
863
+ * Tri-state physical-home compare. A realpath failure (EACCES, EPERM, a
864
+ * vanished directory, transient I/O) is "unknown", not "different": callers
865
+ * deciding whether a home is foreign must not turn an unreadable resolution
866
+ * into a definitive mismatch. Lifecycle guards may still fail closed on
867
+ * "unknown".
868
+ */
869
+ export function compareServicePathToInstall(recorded: string, current: string, deps: CodexHomeDeps = {}): ServicePathComparison {
870
+ if (serviceHomeMatches(recorded, current)) return "same";
871
+ const realpath = deps.realpathSync ?? realpathSync;
872
+ let currentPhysical: string;
873
+ try {
874
+ currentPhysical = realpath(current);
875
+ } catch {
876
+ return "unknown";
877
+ }
878
+ try {
879
+ return serviceHomeMatches(realpath(recorded), currentPhysical) ? "same" : "different";
880
+ } catch (error) {
881
+ // The spellings already differ. A recorded path that no longer exists cannot be an alias of
882
+ // the current home, so it stays a mismatch (a stale mount keeps the foreign-owner refusal);
883
+ // only an error that leaves existence unproven, such as EACCES, is indeterminate.
884
+ const code = (error as NodeJS.ErrnoException | null)?.code;
885
+ return code === "ENOENT" || code === "ENOTDIR" ? "different" : "unknown";
886
+ }
887
+ }
888
+
889
+ /** Lexical compare first; when spellings differ, compare the directories both resolve to so a
890
+ * junction or symlink spelling recorded by an older install still names the same home.
891
+ * Fails closed on an indeterminate resolution — ownership classification needs the
892
+ * tri-state {@link compareServicePathToInstall} instead. */
893
+ export function servicePathMatchesInstall(recorded: string, current: string, deps: CodexHomeDeps = {}): boolean {
894
+ return compareServicePathToInstall(recorded, current, deps) === "same";
895
+ }
896
+
860
897
  export function serviceCodexHomeMatchesInstall(recordedHome: string, deps: CodexHomeDeps = {}): boolean {
861
- return serviceHomeMatches(recordedHome, currentCodexHome(deps));
898
+ return servicePathMatchesInstall(recordedHome, currentCodexHome(deps), deps);
862
899
  }
863
900
 
864
901
  /** Single accessor for backend-sensitive service code — v1/legacy state maps to scheduler. */
package/src/service.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * restore it via the command.
7
7
  */
8
8
 
9
- export type { ServiceBackend, ServiceInstallState, ServiceStateEvidence, ServiceStateResolution, ServiceOwner, ServiceOwnership, ServiceOwnershipSubject, ServiceOwnershipResolution, ServiceStateSwapDeps, RecordServiceOwnerRequest, RecordServiceOwnerDeps, ReleaseServiceOwnerDeps, RemoveServiceStateDeps } from "./service/state";
10
- export { SERVICE_MANAGED_ENV, SERVICE_OWNERSHIP_PROTOCOL_VERSION, SERVICE_OWNERSHIP_MINIMUM_CLI_VERSION, stableLauncherEntry, serviceLogPath, serviceStatePaths, serviceStatePathsForOpenCodexHome, parseServiceInstallState, parseServiceOwnership, inspectServiceStateEvidence, resolveServiceState, currentServiceHomes, serviceHomeMatches, serviceCodexHomeMatchesInstall, readServiceBackend, serviceReinstallArgs, serviceInstallArgs, ServiceStateConflictError, ServiceOwnershipSubjectMismatchError, ServiceOwnershipSubjectUnknownError, ServiceTakeoverCompatibilityChangedError, swapServiceInstallState, removeServiceInstallStateRecords, serviceOwnership, resolveServiceOwnership, sameServiceOwnershipSubject, desktopOwnsService, ownershipGrantedTo, recordServiceOwner, releaseServiceOwner } from "./service/state";
9
+ export type { ServiceBackend, ServiceInstallState, ServiceStateEvidence, ServiceStateResolution, ServiceOwner, ServiceOwnership, ServiceOwnershipSubject, ServiceOwnershipResolution, ServicePathComparison, ServiceStateSwapDeps, RecordServiceOwnerRequest, RecordServiceOwnerDeps, ReleaseServiceOwnerDeps, RemoveServiceStateDeps } from "./service/state";
10
+ export { SERVICE_MANAGED_ENV, SERVICE_OWNERSHIP_PROTOCOL_VERSION, SERVICE_OWNERSHIP_MINIMUM_CLI_VERSION, stableLauncherEntry, serviceLogPath, serviceStatePaths, serviceStatePathsForOpenCodexHome, parseServiceInstallState, parseServiceOwnership, inspectServiceStateEvidence, resolveServiceState, currentServiceHomes, serviceHomeMatches, serviceCodexHomeMatchesInstall, servicePathMatchesInstall, compareServicePathToInstall, readServiceBackend, serviceReinstallArgs, serviceInstallArgs, ServiceStateConflictError, ServiceOwnershipSubjectMismatchError, ServiceOwnershipSubjectUnknownError, ServiceTakeoverCompatibilityChangedError, swapServiceInstallState, removeServiceInstallStateRecords, serviceOwnership, resolveServiceOwnership, sameServiceOwnershipSubject, desktopOwnsService, ownershipGrantedTo, recordServiceOwner, releaseServiceOwner } from "./service/state";
11
11
  export type { OwnershipMutationLeaseOptions, OwnershipMutationLease } from "./service/ownership-mutation-lease.mjs";
12
12
  export { acquireOwnershipMutationLease, withOwnershipMutationLease } from "./service/ownership-mutation-lease.mjs";
13
13
  export type { ManagingCliRole, ManagingCliObservation, RegisteredManagingCliInvocation, ServiceTakeoverCompatibilityInput, ServiceTakeoverCompatibility } from "./service/ownership-compatibility";
@@ -362,6 +362,18 @@ export interface OcxConfigRebaseProvenance {
362
362
  deletedTopLevelKeys: string[];
363
363
  }
364
364
 
365
+
366
+ export type SkillsCatalogRefresh = "per_session" | "per_turn";
367
+
368
+ export interface OcxSkillsConfig {
369
+ /**
370
+ * Refresh policy for the runtime skills catalog (#5569).
371
+ * `per_session` (default): snapshots incoming `<skills_instructions>` on the first turn of a trustworthy session and reuses it across turns to preserve the Anthropic prompt cache.
372
+ * `per_turn`: re-derives/passes through incoming skills instructions every turn (previous behavior).
373
+ */
374
+ catalog_refresh?: SkillsCatalogRefresh;
375
+ }
376
+
365
377
  export type OcxRuntimeRole = "standalone" | "hub" | "client";
366
378
 
367
379
  export interface OcxHubConfig {
@@ -487,6 +499,8 @@ export interface OcxConfig {
487
499
  client?: OcxClientConnectionConfig;
488
500
  /** Operator-facing redaction policy for management and CLI projections. */
489
501
  privacy?: OcxPrivacyConfig;
502
+ /** Runtime skills catalog session snapshotting settings (#5569). */
503
+ skills?: OcxSkillsConfig;
490
504
  /** Opt-in process-local aggregate request metrics on the authenticated management plane. */
491
505
  metricsExport?: { enabled?: boolean };
492
506
  /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
@@ -724,6 +738,27 @@ export interface OcxConfig {
724
738
  };
725
739
  /** Opt-in failure-only recovery; never replaces the initial compaction model. */
726
740
  compactionRecovery?: { enabled: boolean; model: string; allowDevinInvalidArgument?: boolean };
741
+ /**
742
+ * Destination model for Codex's own memory pipeline, per phase
743
+ * (src/server/responses/memory-models.ts).
744
+ *
745
+ * Codex runs Phase 1 ("extract") once per finished thread to summarize that thread's rollout,
746
+ * and Phase 2 ("consolidation") once as an agent run that merges the summaries into the files
747
+ * under `$CODEX_HOME/memories`. Without an entry here each phase keeps its existing route,
748
+ * including any configured shadow-call interception.
749
+ *
750
+ * A phase is recognized from Codex's turn metadata, never inferred from the model id, the timing
751
+ * or the token counts: Phase 1 shares `gpt-5.6-luna` with the app's title/commit helper calls,
752
+ * and `shadowCallIntercept` is the setting for those. A configured phase wins over that
753
+ * intercept, because the memory decision is the more specific one.
754
+ *
755
+ * `model` is required for a configured phase; omitting the phase leaves its route in place.
756
+ * `reasoningEffort` overrides the effort Codex hard-codes for that phase.
757
+ */
758
+ memoryModels?: {
759
+ extract?: { model: string; reasoningEffort?: string };
760
+ consolidation?: { model: string; reasoningEffort?: string };
761
+ };
727
762
  /**
728
763
  * Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids
729
764
  * are excluded from the catalog + /v1/models entirely. Account-qualified native ids hide only
@@ -869,10 +904,12 @@ export interface OcxConfig {
869
904
  * HTTP URLs are mirrored into HTTP_PROXY/HTTPS_PROXY when unset. SOCKS5 URLs are mirrored
870
905
  * into ALL_PROXY, clear inherited HTTP(S)_PROXY, and use OpenCodex's SOCKS5 transport.
871
906
  * Loopback stays in NO_PROXY.
872
- * The literal `"auto"` reads the Windows WinINET static proxy (`ProxyEnable`/`ProxyServer`)
873
- * once at process start, preserving separate HTTP and HTTPS entries; on other platforms, or
874
- * when the system proxy is off, SOCKS-only, or unreadable, it degrades to direct egress with
875
- * one log line (#1525). PAC/WPAD and live changes are not followed.
907
+ * The literal `"auto"` reads Windows WinINET or macOS static HTTP/HTTPS proxy settings
908
+ * once at startup. Inherited scheme proxies win; on macOS, inherited ALL_PROXY also skips
909
+ * discovery. A macOS `*.<domain>` exception maps to `.<domain>` (including the apex),
910
+ * exact link-local CIDRs are omitted with a warning, and other unsafe exceptions
911
+ * refuse discovery without environment writes.
912
+ * PAC/WPAD, SOCKS-only settings, and live changes are not followed.
876
913
  */
877
914
  proxy?: string;
878
915
  /**
@@ -1226,6 +1263,11 @@ export interface OcxComboTarget {
1226
1263
  * target currently advertises; an explicit list must be non-empty.
1227
1264
  */
1228
1265
  reasoningEfforts?: OcxComboDefaultEffort[];
1266
+ /**
1267
+ * Operator-authored capability description sent only to the JEV decision
1268
+ * service for this target. The built-in model profile always applies.
1269
+ */
1270
+ modelProfile?: string;
1229
1271
  /**
1230
1272
  * Marks an emergency-only target. Inert unless the combo sets
1231
1273
  * `cooldownWaitPolicy`, and never makes a target permanently ineligible —
@@ -218,6 +218,8 @@ export interface AttemptTierOutcome {
218
218
  callerFastSuppressedByConfig?: boolean;
219
219
  confirmation: "confirmed" | "assumed" | "downgraded" | "unknown";
220
220
  responseServiceTier?: string;
221
+ /** False retains the raw echo as evidence only, including during cost estimation. */
222
+ responseTierAuthoritative?: boolean;
221
223
  }
222
224
 
223
225
  /**
@@ -307,6 +309,13 @@ export interface OcxProviderConfig {
307
309
  * absence derives from the final model adapter.
308
310
  */
309
311
  fastWire?: FastWire | null;
312
+ /**
313
+ * Whether echoed service_tier can confirm or deny Fast. Set false for a relay whose
314
+ * response metadata cannot establish the granted tier. Absence keeps legacy authority;
315
+ * canonical ChatGPT Codex forwarding always treats the echo as non-authoritative.
316
+ * Observation only: this does not enable Fast or change request serialization.
317
+ */
318
+ responseTierAuthoritative?: boolean;
310
319
  baseUrl: string;
311
320
  /**
312
321
  * Optional relative resource path for key-auth openai-responses requests. Must start with `/`
@@ -143,6 +143,12 @@ export interface OcxParsedRequest {
143
143
  _compactionRequest?: boolean;
144
144
  /** Manual compaction moved to another provider: summarize portably even on a canonical ChatGPT target. */
145
145
  _portableCompaction?: boolean;
146
+ /**
147
+ * Codex memory pipeline phase this turn belongs to, when `memoryModels` routes it
148
+ * (src/server/responses/memory-models.ts). Read at the effort choke point, which runs after the
149
+ * route is known.
150
+ */
151
+ _memoryModelPhase?: "extract" | "consolidation";
146
152
  /**
147
153
  * True when the current request newly introduced a stored compaction summary/marker. Historical
148
154
  * markers restored by previous_response_id expansion were already acknowledged and do not reset
@@ -70,8 +70,9 @@ export function dottedToolName(namespace: string | undefined, name: string): str
70
70
  * `apply_patch`, `view_image`, or one of the goal helpers (`create_goal`, `get_goal`,
71
71
  * `update_goal`, #5495) instead of the declared `exec`. Accept these nested helper names only
72
72
  * when the request catalog actually declares `exec` and does not itself declare the emitted name
73
- * (an MCP server may legitimately advertise one under its own namespace). The list is closed: an
74
- * unlisted name is never admitted through `exec`.
73
+ * (an MCP server may legitimately advertise one under its own namespace). The helper list itself
74
+ * is closed; the one other admission through `exec` is a direct `mcp__<server>__<tool>` call,
75
+ * which `isCodeModeMcpDirectName` recognizes.
75
76
  */
76
77
  const LEGACY_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
77
78
  const CODE_MODE_HELPER_TOOL_NAMES = [
@@ -105,6 +106,23 @@ export const CODE_MODE_HELPER_WIRE_NAMES: ReadonlySet<string> = new Set<string>(
105
106
  CODE_MODE_HELPER_TOOL_NAMES,
106
107
  );
107
108
 
109
+ /**
110
+ * A flattened MCP wire name (`mcp__<server>__<tool>`) emitted as a direct tool call.
111
+ *
112
+ * Codex code mode reaches the host's nested tools through `tools.<name>(...)` inside `exec`,
113
+ * so none of them are declared; routed models (observed: Kimi K3, GLM 5.3) occasionally skip the
114
+ * wrapper and call the flattened name directly. Under a code-mode catalog the call is compiled
115
+ * into the equivalent `tools.<name>(...)` exec body instead of failing closed — capability-
116
+ * equivalent, since the model could have written that JavaScript itself. Server and tool must
117
+ * both be non-empty so a bare `mcp__` prefix never qualifies.
118
+ */
119
+ export function isCodeModeMcpDirectName(name: string): boolean {
120
+ if (!name.startsWith("mcp__")) return false;
121
+ const rest = name.slice("mcp__".length);
122
+ const separator = rest.indexOf("__");
123
+ return separator > 0 && separator + "__".length < rest.length;
124
+ }
125
+
108
126
  /**
109
127
  * Spellings that may never be MANUFACTURED as a bare alias for a namespaced tool.
110
128
  *
@@ -137,19 +155,24 @@ export const NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES: ReadonlySet<string> = new Set
137
155
  * The same wrapper may surround an already-flattened namespace identity; accept that exact
138
156
  * declared suffix without treating its child name as a bare declaration.
139
157
  * Also normalizes nested helper names (`exec_command`, `shell_command`, `write_stdin`,
140
- * `apply_patch`, `view_image`, `create_goal`, `get_goal`, `update_goal`) to
141
- * `exec` when code-mode `exec` is declared in the request catalog.
158
+ * `apply_patch`, `view_image`, `create_goal`, `get_goal`, `update_goal`) and direct
159
+ * `mcp__<server>__<tool>` calls to `exec` when code-mode `exec` is declared in the
160
+ * request catalog. MCP recovery additionally requires explicit custom-tool provenance;
161
+ * a structured function named `exec` is not a JavaScript executor.
142
162
  *
143
163
  * @param name - The tool name emitted on the wire by the provider.
144
164
  * @param declared - All wire tool names declared in the request catalog, including aliases.
145
165
  * @param declaredBare - Explicitly declared bare tool names without namespace provenance.
146
166
  * When omitted, falls back to `declared`.
167
+ * @param declaredCustom - Custom wire identities from the caller's catalog, never bare aliases
168
+ * manufactured from foreign namespaces.
147
169
  * @returns The normalized tool name to expose downstream.
148
170
  */
149
171
  export function normalizeDeclaredToolName(
150
172
  name: string,
151
173
  declared: ReadonlySet<string> | undefined,
152
174
  declaredBare?: ReadonlySet<string>,
175
+ declaredCustom?: ReadonlySet<string>,
153
176
  ): string {
154
177
  if (!declared) return name;
155
178
  if (declared.has(name)) return name;
@@ -172,11 +195,12 @@ export function normalizeDeclaredToolName(
172
195
  candidate = bare;
173
196
  } else if (
174
197
  // Code mode never declares bare helper names; a provider that invents `default.`
175
- // for one still means the nested helper. Strip the prefix so the helper list
176
- // below can rewrite it to `exec` (#4412).
198
+ // for one still means the nested helper. The same wrapper can surround a direct MCP
199
+ // name, but only a custom exec declaration authorizes that recovery.
177
200
  bare.length > 0
178
201
  && declared.has(CODE_MODE_EXEC_TOOL_NAME)
179
- && (CODE_MODE_HELPER_TOOL_NAMES as readonly string[]).includes(bare)
202
+ && ((CODE_MODE_HELPER_TOOL_NAMES as readonly string[]).includes(bare)
203
+ || (declaredCustom?.has(CODE_MODE_EXEC_TOOL_NAME) && isCodeModeMcpDirectName(bare)))
180
204
  && !declared.has("default." + bare)
181
205
  && !declared.has("default__" + bare)
182
206
  ) {
@@ -192,7 +216,13 @@ export function normalizeDeclaredToolName(
192
216
  if ((LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).some(legacy => declared.has(legacy))) {
193
217
  return candidate;
194
218
  }
195
- return (CODE_MODE_HELPER_TOOL_NAMES as readonly string[]).includes(candidate)
219
+ if ((CODE_MODE_HELPER_TOOL_NAMES as readonly string[]).includes(candidate)) {
220
+ return CODE_MODE_EXEC_TOOL_NAME;
221
+ }
222
+ // A direct `mcp__<server>__<tool>` call names a nested host tool the code-mode catalog
223
+ // never declares; `compileCodeModeHelperInput` turns it into the `tools.<name>(...)`
224
+ // exec body the model could have written itself.
225
+ return declaredCustom?.has(CODE_MODE_EXEC_TOOL_NAME) && isCodeModeMcpDirectName(candidate)
196
226
  ? CODE_MODE_EXEC_TOOL_NAME
197
227
  : candidate;
198
228
  }
package/src/types.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  dottedToolName,
9
9
  namespacedToolName,
10
10
  normalizeDeclaredToolName,
11
+ isCodeModeMcpDirectName,
11
12
  toolChoiceAliases,
12
13
  createToolChoiceResolver,
13
14
  toolChoiceCandidates,
@@ -77,6 +78,8 @@ export type {
77
78
  OcxConnectedClientId,
78
79
  OcxClientConnectionConfig,
79
80
  OcxConfig,
81
+ SkillsCatalogRefresh,
82
+ OcxSkillsConfig,
80
83
  OcxAccountPoolRotationStrategy,
81
84
  OcxAccountPoolQuotaWindow,
82
85
  OcxComboCooldownWaitPolicy,
@@ -2,6 +2,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { unprivilegedOwnershipMutationEnvironment } from "../service/ownership-mutation-lease.mjs";
3
3
  import { PKG, registrySpawnTarget, type Channel, type Installer } from "./index";
4
4
  import type { PnpmGlobalOwner } from "./pnpm-global-install.mjs";
5
+ import { PNPM_READ_CWD, pnpmReadEnvironment } from "./pnpm-read-policy.mjs";
5
6
 
6
7
  export const REGISTRY_DEADLINE_MS = 12_000;
7
8
  export const REGISTRY_OUTPUT_LIMIT = 4_096;
@@ -64,7 +65,10 @@ export async function latestVersionAsync(
64
65
  child = deps.spawnFn(target.bin, target.args, {
65
66
  stdio: ["pipe", "pipe", "pipe"],
66
67
  windowsHide: true,
67
- env: unprivilegedOwnershipMutationEnvironment(target.env ?? process.env),
68
+ cwd: installer === "pnpm" ? PNPM_READ_CWD : undefined,
69
+ env: installer === "pnpm"
70
+ ? pnpmReadEnvironment(unprivilegedOwnershipMutationEnvironment(target.env ?? process.env))
71
+ : unprivilegedOwnershipMutationEnvironment(target.env ?? process.env),
68
72
  ...target.options,
69
73
  }) as ChildProcessWithoutNullStreams;
70
74
  } catch {
@@ -41,6 +41,7 @@ import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
41
41
  import { withoutSiblingMarker } from "../codex/sibling-start";
42
42
  import { packageVersion } from "../lib/package-version";
43
43
  import { selfLaunchArgv } from "../lib/self-launch-argv";
44
+ import { PNPM_READ_CWD, withPnpmCommandCwd, pnpmReadEnvironment } from "./pnpm-read-policy.mjs";
44
45
 
45
46
  /**
46
47
  * A `codex-history-backup-*.json` surviving a stop means the native-history restore was
@@ -98,27 +99,33 @@ function runPnpmCandidate(
98
99
  commandPath: string,
99
100
  args: readonly string[],
100
101
  capture = false,
102
+ spawn: typeof spawnSync = spawnSync,
101
103
  ): { status: number | null; stdout?: string | null; stderr?: string | null } {
102
104
  const invocation = pnpmInvocationForPath(commandPath, args);
103
105
  if (!invocation) return { status: 1 };
104
- return spawnSync(invocation.file, invocation.args, {
106
+ return spawn(invocation.file, invocation.args, {
105
107
  stdio: capture ? "pipe" : "ignore",
106
108
  encoding: "utf8",
107
109
  timeout: 20_000,
108
110
  windowsHide: true,
109
- env: unprivilegedOwnershipMutationEnvironment(process.env),
111
+ cwd: PNPM_READ_CWD,
112
+ env: pnpmReadEnvironment(unprivilegedOwnershipMutationEnvironment(process.env)),
110
113
  ...invocation.options,
111
114
  });
112
115
  }
113
116
 
114
117
  /** Resolve the exact pnpm executable/group/bin that own this package. */
115
- export function resolveCurrentPnpmGlobalOwner(invoked = process.argv[1]): PnpmGlobalOwnerResult {
118
+ export function resolveCurrentPnpmGlobalOwner(
119
+ invoked = process.argv[1],
120
+ deps: { commandPaths?: readonly string[]; spawn?: typeof spawnSync } = {},
121
+ ): PnpmGlobalOwnerResult {
122
+ const spawn = deps.spawn ?? spawnSync;
116
123
  return resolvePnpmGlobalOwner({
117
124
  packageName: PKG,
118
125
  packagePath: packageRoot(),
119
- commandPaths: resolvePnpmCommands(),
126
+ commandPaths: deps.commandPaths ?? resolvePnpmCommands(),
120
127
  runningShimPath: runningPnpmShimPath(invoked),
121
- runPnpm: runPnpmCandidate,
128
+ runPnpm: (commandPath, args, capture) => runPnpmCandidate(commandPath, args, capture, spawn),
122
129
  });
123
130
  }
124
131
 
@@ -136,22 +143,26 @@ function ownerPnpmTarget(
136
143
  };
137
144
  }
138
145
 
139
- function runOwnedPnpm(
146
+ export function runOwnedPnpm(
140
147
  owner: PnpmGlobalOwner,
141
148
  args: readonly string[],
142
149
  capture: boolean,
143
150
  stdio: "inherit" | "pipe" | "ignore" = capture ? "pipe" : "inherit",
151
+ spawn: typeof spawnSync = spawnSync,
144
152
  ): { status: number | null; stdout?: string | null; stderr?: string | null } {
145
153
  const target = ownerPnpmTarget(owner, args);
146
154
  if (!target) return { status: 1 };
147
- return spawnSync(target.bin, target.args, {
155
+ return withPnpmCommandCwd(args, cwd => spawn(target.bin, target.args, {
148
156
  stdio,
149
157
  encoding: "utf8",
150
158
  timeout: 180_000,
151
159
  windowsHide: true,
152
- env: unprivilegedOwnershipMutationEnvironment(target.env),
160
+ // Reads probe from the package dir; `add -g`/rollback children run from a neutral
161
+ // directory so a Windows cwd handle never pins open the package pnpm is replacing.
162
+ cwd,
163
+ env: pnpmReadEnvironment(unprivilegedOwnershipMutationEnvironment(target.env)),
153
164
  ...target.options,
154
- });
165
+ }));
155
166
  }
156
167
 
157
168
  /** Re-read the owning group's active package and return its verified launcher. */
@@ -276,16 +287,20 @@ export function latestVersion(
276
287
  tag: string,
277
288
  installer: Installer = detectInstall(),
278
289
  owner?: PnpmGlobalOwner,
290
+ spawn: typeof spawnSync = spawnSync,
279
291
  ): string | null {
280
292
  const resolvedOwner = installer === "pnpm" ? selectedPnpmOwner(owner) : undefined;
281
293
  if (installer === "pnpm" && !resolvedOwner) return null;
282
294
  const manager = registrySpawnTarget(installer, ["view", `${PKG}@${tag}`, "version"], resolvedOwner);
283
295
  if (!manager) return null;
284
- const r = spawnSync(manager.bin, manager.args, {
296
+ const r = spawn(manager.bin, manager.args, {
285
297
  encoding: "utf8",
286
298
  timeout: 12000,
287
299
  windowsHide: true,
288
- env: unprivilegedOwnershipMutationEnvironment(manager.env ?? process.env),
300
+ cwd: installer === "pnpm" ? PNPM_READ_CWD : undefined,
301
+ env: installer === "pnpm"
302
+ ? pnpmReadEnvironment(unprivilegedOwnershipMutationEnvironment(manager.env ?? process.env))
303
+ : unprivilegedOwnershipMutationEnvironment(manager.env ?? process.env),
289
304
  ...manager.options,
290
305
  });
291
306
  return r.status === 0 && typeof r.stdout === "string" ? (r.stdout.trim() || null) : null;
@@ -342,7 +357,10 @@ export function checkUpdatePackageIntegrity(
342
357
  encoding: "utf8",
343
358
  timeout: 12000,
344
359
  windowsHide: true,
345
- env: unprivilegedOwnershipMutationEnvironment(target.env ?? process.env),
360
+ cwd: installer === "pnpm" ? PNPM_READ_CWD : undefined,
361
+ env: installer === "pnpm"
362
+ ? pnpmReadEnvironment(unprivilegedOwnershipMutationEnvironment(target.env ?? process.env))
363
+ : unprivilegedOwnershipMutationEnvironment(target.env ?? process.env),
346
364
  ...target.options,
347
365
  });
348
366
  });
package/src/update/job.ts CHANGED
@@ -62,6 +62,7 @@ import {
62
62
  } from "./npm-cache-preflight.mjs";
63
63
  import { guiUpdateWorkerCommand } from "./worker-launch";
64
64
  import { withoutSiblingMarker } from "../codex/sibling-start";
65
+ import type { WorkerLaunchContext } from "./worker-launch";
65
66
 
66
67
  const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
67
68
  const UPDATE_JOB_FILENAME = "update-job.json";
@@ -568,6 +569,7 @@ export function spawnGuiUpdateWorker(
568
569
  jobId: string,
569
570
  channel: Channel,
570
571
  restart: boolean,
572
+ context: WorkerLaunchContext = {},
571
573
  ): UpdateWorkerProcess {
572
574
  const args = selfLaunchArgv([
573
575
  "__gui-update-worker",
@@ -576,7 +578,7 @@ export function spawnGuiUpdateWorker(
576
578
  restart ? "restart" : "no-restart",
577
579
  ]);
578
580
  if (process.platform !== "win32") {
579
- const launch = guiUpdateWorkerCommand(process.execPath, args);
581
+ const launch = guiUpdateWorkerCommand(process.execPath, args, context);
580
582
  return spawn(launch.command, launch.argv, {
581
583
  detached: true,
582
584
  stdio: "ignore",
@@ -0,0 +1,4 @@
1
+ export declare const PNPM_READ_CWD: string;
2
+ /** The callback must complete synchronously before its temporary workspace is cleaned. */
3
+ export declare function withPnpmCommandCwd<T>(args: readonly string[], run: (cwd: string) => T): T;
4
+ export declare function pnpmReadEnvironment(env?: Record<string, string | undefined>): Record<string, string | undefined>;