@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
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
6
+ import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
6
7
  import { commandInvocation } from "../../lib/win-exec";
7
8
  import { isStandaloneBinary } from "../../lib/standalone";
8
9
  import { modelRecordValue } from "../../reasoning-effort";
@@ -10,9 +11,11 @@ import type { IncomingMeta } from "../base";
10
11
  import {
11
12
  buildConversationInput,
12
13
  CodingAgentProtocolError,
14
+ MAX_TOOL_BLOCK_STARTS,
13
15
  mapStreamMessageToEvents,
14
16
  projectedHistoryCharLimit,
15
17
  readJsonLines,
18
+ releaseOpenToolBlocks,
16
19
  toolBridgeInitError,
17
20
  type StreamParseState,
18
21
  } from "./protocol";
@@ -369,6 +372,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
369
372
  };
370
373
 
371
374
  let streamProtocolError: string | undefined;
375
+ let streamProtocolCode: string | undefined;
372
376
  let turnError: string | undefined;
373
377
  // A successful result frame that arrived after every captured tool call completed but
374
378
  // before message_stop. The bridge contract still ends the leg with the synthesized
@@ -381,6 +385,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
381
385
  sawPartialThinking: false,
382
386
  sawTerminalResult: false,
383
387
  openToolBlocks: new Map(),
388
+ translatorBudget: incoming.translatorBudget,
389
+ maxToolBlockStarts: toolBridge?.maxTurnToolCalls,
390
+ strictToolBlockCapture: Boolean(toolBridge),
384
391
  partialToolCallIds: toolBridge ? new Set<string>() : undefined,
385
392
  };
386
393
 
@@ -403,7 +410,6 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
403
410
  if (!stdout) throw new CodingAgentProtocolError(`${profile.label} CLI produced no stdout stream`);
404
411
  try {
405
412
  let initValidated = false;
406
- let toolCallStarts = 0;
407
413
  let failClosed = false;
408
414
  for await (const message of readJsonLines(stdout)) {
409
415
  if (incoming.abortSignal?.aborted) break;
@@ -447,6 +453,18 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
447
453
  }
448
454
  }
449
455
  const mappedEvents = mapStreamMessageToEvents(message, state);
456
+ if (state.toolCallLimitExceeded) {
457
+ emitOnce({
458
+ type: "error",
459
+ message: `Coding-agent CLI returned more than the ${Math.min(MAX_TOOL_BLOCK_STARTS, state.maxToolBlockStarts ?? MAX_TOOL_BLOCK_STARTS)}-tool-call turn limit.`,
460
+ status: 502,
461
+ errorType: "upstream_error",
462
+ code: "tool_call_limit",
463
+ retryable: false,
464
+ });
465
+ kill();
466
+ break;
467
+ }
450
468
  if (toolBridge && state.uncapturedToolUse) {
451
469
  emitOnce({
452
470
  type: "error",
@@ -474,20 +492,6 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
474
492
  break;
475
493
  }
476
494
  if (toolBridge && event.type === "tool_call_start") {
477
- toolCallStarts += 1;
478
- if (toolCallStarts > toolBridge.maxTurnToolCalls) {
479
- emitOnce({
480
- type: "error",
481
- message: `Coding-agent CLI returned more than the ${toolBridge.maxTurnToolCalls}-tool-call turn limit.`,
482
- status: 502,
483
- errorType: "upstream_error",
484
- code: "tool_call_limit",
485
- retryable: false,
486
- });
487
- failClosed = true;
488
- kill();
489
- break;
490
- }
491
495
  const wireName = toolBridge.emittedNameMap.get(event.name);
492
496
  if (wireName === undefined) {
493
497
  emitOnce({
@@ -593,8 +597,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
593
597
  break;
594
598
  }
595
599
  if (toolBridge && !terminalEmitted && state.sawMessageStop && (state.completedToolCalls ?? 0) > 0) {
596
- // Raw tool_use starts are gated before buffering, so every completed call was admitted
597
- // after the bridge init handshake.
600
+ // The raw-start gate above already refused any call opened before the handshake.
598
601
  // The capture-only MCP handler never answers, so the CLI parks after message_stop.
599
602
  // The completed tool_use blocks are this turn's structured output: end the leg here
600
603
  // and terminate the tree; the client executes, and the next request continues.
@@ -615,11 +618,13 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
615
618
  } catch (err) {
616
619
  kill();
617
620
  streamProtocolError = err instanceof Error ? err.message : String(err);
621
+ if (isTranslatorBudgetExceededError(err)) streamProtocolCode = err.code;
618
622
  }
619
623
  } catch (err) {
620
624
  kill();
621
625
  turnError = err instanceof Error ? err.message : String(err);
622
626
  } finally {
627
+ releaseOpenToolBlocks(state);
623
628
  cleanup();
624
629
  if (toolBridgeDir) {
625
630
  await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
@@ -666,7 +671,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
666
671
  message: redactSecrets(streamProtocolError, profile.tokenEnv, apiKey),
667
672
  status: 502,
668
673
  errorType: "upstream_error",
669
- code: "protocol_error",
674
+ code: streamProtocolCode ?? "protocol_error",
670
675
  retryable: false,
671
676
  });
672
677
  } else if (child.exitCode !== null && child.exitCode !== 0) {
@@ -1,6 +1,19 @@
1
1
  import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
2
2
  import { isLocationUnsupportedMessage } from "../lib/errors";
3
3
 
4
+ export const ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX = "Antigravity account validation required (VALIDATION_REQUIRED)";
5
+
6
+ function hasAntigravityValidationReason(payloadText: string): boolean {
7
+ const payload = parseUpstreamJsonPayload(payloadText);
8
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
9
+ const error = (payload as { error?: unknown }).error;
10
+ if (!error || typeof error !== "object" || Array.isArray(error)) return false;
11
+ const details = (error as { details?: unknown }).details;
12
+ return Array.isArray(details) && details.some(detail =>
13
+ detail !== null && typeof detail === "object" && !Array.isArray(detail)
14
+ && (detail as { reason?: unknown }).reason === "VALIDATION_REQUIRED");
15
+ }
16
+
4
17
  /** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
5
18
  function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
6
19
  const trimmed = payloadText.trim();
@@ -98,7 +111,9 @@ function classifyGoogle(label: string, status: number | undefined, enumStatus: s
98
111
  */
99
112
  export function safeGoogleHttpErrorMessage(label: string, status: number, payloadText: string): string {
100
113
  const { message, status: enumStatus } = googleErrorDetail(payloadText);
101
- const prefix = classifyGoogle(label, status, enumStatus, [message, enumStatus].filter(Boolean).join(" "));
114
+ const prefix = label === "Antigravity" && status === 403 && hasAntigravityValidationReason(payloadText)
115
+ ? ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX
116
+ : classifyGoogle(label, status, enumStatus, [message, enumStatus].filter(Boolean).join(" "));
102
117
  const detail = message ? sanitizeUpstreamErrorText(message).slice(0, 500) : `HTTP ${status}`;
103
118
  return `${prefix}: ${detail}`;
104
119
  }
@@ -7,6 +7,7 @@ import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage
7
7
  import { repairGoogleInvalidRequestBodyWithReport } from "./google-wire-compiler";
8
8
  import type { GoogleToolSchemaPolicy, GoogleToolSchemaProfile } from "./google-tool-schema";
9
9
  import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
10
+ import { readBoundedResponseBody } from "../lib/bounded-body";
10
11
  import {
11
12
  abortError,
12
13
  cancelResponseBodyBestEffort,
@@ -29,6 +30,19 @@ export interface GoogleRetryOptions {
29
30
  }
30
31
 
31
32
  async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
33
+ if (label === "Antigravity" && res.status === 403) {
34
+ const body = await readBoundedResponseBody(res, {
35
+ maxBytes: 4096, totalTimeoutMs: 2000, firstByteTimeoutMs: 2000,
36
+ inactivityTimeoutMs: 2000, signal,
37
+ });
38
+ const headers = new Headers(res.headers);
39
+ headers.delete("content-encoding");
40
+ headers.delete("content-length");
41
+ return new Response(safeGoogleHttpErrorMessage(label, res.status,
42
+ body.displaySafe && !body.truncated ? body.text : ""), {
43
+ status: res.status, statusText: res.statusText, headers,
44
+ });
45
+ }
32
46
  return normalizeUpstreamHttpErrorResponse(res, {
33
47
  signal,
34
48
  formatMessage: payloadText => safeGoogleHttpErrorMessage(label, res.status, payloadText),
@@ -1,3 +1,4 @@
1
+ import { protectGlmSummaryBudget } from "./summary-budget";
1
2
  import { openAIChatTransport, stripBracketedModelSuffix } from "./wire";
2
3
  import type { AdapterRequest } from "../base";
3
4
  import { frameAgentRouterMessages } from "../agentrouter";
@@ -72,6 +73,7 @@ export function buildOpenAIChatPassthroughRequest(
72
73
  for (const field of CHAT_PASSTHROUGH_FIELDS) {
73
74
  if (rawBody[field] !== undefined) body[field] = rawBody[field];
74
75
  }
76
+ if (protectGlmSummaryBudget(body, provider.baseUrl, body.reasoning_effort)) body.reasoning_effort = "low";
75
77
  const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
76
78
  const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
77
79
  if (reasoningDisabled) {
@@ -0,0 +1,72 @@
1
+ import { modelRecordValue } from "../../reasoning-effort";
2
+ import type { OcxParsedRequest, OcxProviderConfig } from "../../types";
3
+
4
+ export function resolveMaxTokens(provider: OcxProviderConfig, parsed: OcxParsedRequest): number | undefined {
5
+ return parsed.options.maxOutputTokens
6
+ ?? modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
7
+ ?? provider.defaultMaxOutputTokens;
8
+ }
9
+
10
+ function textContent(value: unknown): string | undefined {
11
+ if (typeof value === "string") return value;
12
+ if (!Array.isArray(value)) return undefined;
13
+ const text: string[] = [];
14
+ for (const part of value) {
15
+ if (!part || part.type !== "text" || typeof part.text !== "string") return undefined;
16
+ text.push(part.text);
17
+ }
18
+ return text.join("\n");
19
+ }
20
+
21
+ const PROTECTED_SUMMARY_CAP = 8192;
22
+ /** A real checkpoint carries the conversation it summarizes; a short probe is not one (#5465). */
23
+ const MIN_CHECKPOINT_TRANSCRIPT_CHARS = 2000;
24
+
25
+ /** The mitigation is scoped to Z.AI's own endpoints; another gateway serving the same model id is not. */
26
+ function isZaiEndpoint(baseUrl: string | undefined): boolean {
27
+ if (!baseUrl) return false;
28
+ try {
29
+ const host = new URL(baseUrl).hostname.toLowerCase();
30
+ return host === "z.ai" || host.endsWith(".z.ai");
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function isTinyCap(value: unknown): value is number {
37
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 1024;
38
+ }
39
+
40
+ /**
41
+ * Aside's emergency checkpoint is a standalone summary, not an ordinary short answer (#5465).
42
+ * Runs at the physical Chat destination, after all combo effort overrides, so `effort` is the
43
+ * effective effort. Only the exhausting tiers (`high`/`max`) on Z.AI's GLM-5.3-Flash, for the
44
+ * two-message checkpoint shape with a real `<conversation>` transcript, qualify. Each tiny cap
45
+ * field is raised on its own, so a caller's larger cap is never shrunk.
46
+ */
47
+ export function protectGlmSummaryBudget(
48
+ body: Record<string, unknown>,
49
+ baseUrl: string | undefined,
50
+ effort: unknown,
51
+ ): boolean {
52
+ if (!isZaiEndpoint(baseUrl)) return false;
53
+ if (effort !== "high" && effort !== "max") return false;
54
+ if (typeof body.model !== "string"
55
+ || !/^(?:(?:zai|z-ai|zai-org)\/)?glm-5\.3-flash$/i.test(body.model)) return false;
56
+ if (body.tools !== undefined && (!Array.isArray(body.tools) || body.tools.length > 0)) return false;
57
+ if (!isTinyCap(body.max_tokens) && !isTinyCap(body.max_completion_tokens)) return false;
58
+ const messages = body.messages;
59
+ if (!Array.isArray(messages) || messages.length !== 2) return false;
60
+ const [system, user] = messages;
61
+ if (!system || !user || !["system", "developer"].includes(system.role) || user.role !== "user"
62
+ || system.tool_calls || user.tool_calls || system.function_call || user.function_call) return false;
63
+ const instruction = textContent(system.content);
64
+ const transcript = textContent(user.content);
65
+ if (instruction === undefined || transcript === undefined) return false;
66
+ if (!/\b(?:summari[sz](?:e|ation|er|ing)|summary|checkpoint)\b/i.test(instruction)) return false;
67
+ if (!/<conversation>[\s\S]*<\/conversation>/i.test(transcript)) return false;
68
+ if (transcript.length < MIN_CHECKPOINT_TRANSCRIPT_CHARS) return false;
69
+ if (isTinyCap(body.max_tokens)) body.max_tokens = PROTECTED_SUMMARY_CAP;
70
+ if (isTinyCap(body.max_completion_tokens)) body.max_completion_tokens = PROTECTED_SUMMARY_CAP;
71
+ return true;
72
+ }
@@ -17,6 +17,7 @@ import type { OcxMessage } from "../../types";
17
17
  */
18
18
  export function createToolCallIdReminter(reservedIds: Iterable<string>): (rawId: string) => string {
19
19
  const occupied = new Set(reservedIds);
20
+ const nextSuffixByWidthAndPrefix = new Map<string, number>();
20
21
  return rawId => {
21
22
  if (!occupied.has(rawId)) {
22
23
  occupied.add(rawId);
@@ -25,16 +26,24 @@ export function createToolCallIdReminter(reservedIds: Iterable<string>): (rawId:
25
26
  // A non-conforming source is sanitized, never dropped: the wire still needs an id, and the
26
27
  // occupied check below covers a sanitized form that now equals some other call's id.
27
28
  const base = isConformingToolCallId(rawId) ? rawId : rawId.replace(/[^a-zA-Z0-9_-]/g, "_");
28
- for (let n = 2; ; n++) {
29
+ for (let n = 2; ;) {
29
30
  // Hyphen, not underscore: an id that extends another id as `<earlier>_<digits>` is parsed by
30
31
  // at least one client as a batch sub-call of `<earlier>`, which pairs the second call's
31
32
  // result to the first call. A `-<n>` suffix is in the same id family without that reading.
32
33
  const suffix = `-${n}`;
33
- const candidate = base.slice(0, Math.max(1, MAX_TOOL_CALL_ID_LENGTH - suffix.length)) + suffix;
34
+ // A wider suffix retains less of the base, so siblings that were separate at -9
35
+ // can converge at -10. Resume in the candidate's actual width/prefix domain.
36
+ const prefix = base.slice(0, Math.max(1, MAX_TOOL_CALL_ID_LENGTH - suffix.length));
37
+ const cursorKey = `${suffix.length}:${prefix}`;
38
+ const next = nextSuffixByWidthAndPrefix.get(cursorKey);
39
+ if (next !== undefined && next > n) { n = next; continue; }
40
+ const candidate = prefix + suffix;
41
+ nextSuffixByWidthAndPrefix.set(cursorKey, n + 1);
34
42
  if (!occupied.has(candidate)) {
35
43
  occupied.add(candidate);
36
44
  return candidate;
37
45
  }
46
+ n++;
38
47
  }
39
48
  };
40
49
  }
@@ -1,11 +1,12 @@
1
1
  import { hasShrinkableOpenAIChatImages, normalizeOpenAIChatImages } from "./openai-chat-images";
2
+ import { protectGlmSummaryBudget, resolveMaxTokens } from "./openai-chat/summary-budget";
2
3
  import { chatParallelToolCallsWireValue } from "./openai-chat/parallel-tool-calls";
3
4
  import { applyExplicitChatReasoningWirePolicy } from "./openai-chat/reasoning-wire";
4
5
  import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
5
6
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
6
7
  import { modelInList } from "../types";
7
8
  import { createInlineThinkContentSplitter, splitInlineThinkContent } from "./inline-think-tags";
8
- import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
9
+ import { mapReasoningEffort } from "../reasoning-effort";
9
10
  import { debugProviderDiagnostic } from "../lib/debug";
10
11
  import { sseFieldValue } from "../lib/sse-decoder";
11
12
  import { isDebugEnabled } from "../lib/debug-settings";
@@ -51,12 +52,6 @@ export { stripBracketedModelSuffix } from "./openai-chat/wire";
51
52
  export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough";
52
53
  export { formatOpenAIChatErrorBody } from "./openai-chat/errors";
53
54
 
54
- function resolveMaxTokens(provider: OcxProviderConfig, parsed: OcxParsedRequest): number | undefined {
55
- return parsed.options.maxOutputTokens
56
- ?? modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
57
- ?? provider.defaultMaxOutputTokens;
58
- }
59
-
60
55
  function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: string, maxOutputTokens?: number): number | undefined {
61
56
  if (parsed.options.reasoning === "minimal") return 0;
62
57
  const maxBudget = maxOutputTokens ?? 32768;
@@ -150,12 +145,14 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
150
145
  body.stop = parsed.options.stopSequences;
151
146
  }
152
147
  const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
153
- const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
148
+ const requestedEffort = protectGlmSummaryBudget(body, provider.baseUrl, parsed.options.reasoning)
149
+ ? "low" : parsed.options.reasoning;
150
+ const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, requestedEffort);
154
151
  const explicitReasoning = applyExplicitChatReasoningWirePolicy({
155
152
  provider,
156
153
  modelId: parsed.modelId,
157
154
  hasTools: !!tools,
158
- requestedEffort: parsed.options.reasoning,
155
+ requestedEffort,
159
156
  wireEffort: reasoningEffort,
160
157
  reasoningDisabled,
161
158
  body,
@@ -86,6 +86,8 @@ function buildResponseJSONWithBudget(
86
86
  toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
87
87
  /** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
88
88
  declaredToolNames?: ReadonlySet<string>;
89
+ /** Bare custom declarations; unlike freeformToolNames, excludes foreign namespace children. */
90
+ bareCustomToolNames?: ReadonlySet<string>;
89
91
  /** See `bridgeToResponsesSSE`: enforcement is separate from normalization (#4735). */
90
92
  enforceDeclaredToolNames?: boolean;
91
93
  /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
@@ -451,7 +453,7 @@ function buildResponseJSONWithBudget(
451
453
  rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
452
454
  }
453
455
  flushToolCall();
454
- const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
456
+ const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames, undefined, options?.bareCustomToolNames);
455
457
  if (
456
458
  (options?.enforceDeclaredToolNames === true || options?.declaredToolNames != null)
457
459
  && options?.enforceDeclaredToolNames !== false
package/src/bridge/sse.ts CHANGED
@@ -101,6 +101,8 @@ export function bridgeToResponsesSSE(
101
101
  onUsage?: (usage: OcxUsage | undefined) => void;
102
102
  /** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
103
103
  declaredToolNames?: ReadonlySet<string>;
104
+ /** Bare custom declarations; unlike freeformToolNames, excludes foreign namespace children. */
105
+ bareCustomToolNames?: ReadonlySet<string>;
104
106
  /**
105
107
  * Whether `declaredToolNames` is an authorization boundary this proxy enforces, or only the
106
108
  * catalog used to normalize provider-invented names back to declared ones.
@@ -1013,7 +1015,7 @@ export function bridgeToResponsesSSE(
1013
1015
  rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
1014
1016
  }
1015
1017
  if (currentToolCall) closeCurrentToolCall();
1016
- const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames);
1018
+ const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames, undefined, options?.bareCustomToolNames);
1017
1019
  const codeModeHelperName = effectiveName === "exec" && event.name !== effectiveName
1018
1020
  ? event.name
1019
1021
  : undefined;
@@ -8,7 +8,7 @@ import type { OcxConfig } from "../types";
8
8
  import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
9
9
  import { getConfigDir } from "../config/paths";
10
10
  import { readFileSync, existsSync } from "node:fs";
11
- import { pickerCaCertPath, pickerCaFingerprints, ensurePickerCa, issuePickerLeaf, pickerLeafCertPath } from "./intercept/picker-ca";
11
+ import { pickerCaCertPath, pickerCaFingerprints, ensurePickerCa, issuePickerLeaf, pickerLeafCertPath, publishedPickerCaSha256 } from "./intercept/picker-ca";
12
12
  import { inspectPickerTrust, trustPickerCa, untrustPickerCa } from "./intercept/picker-trust";
13
13
  import type { PickerRuntime } from "./intercept/picker-runtime";
14
14
  import type { PickerTrustState, SecurityRunner } from "./intercept/picker-trust";
@@ -36,6 +36,8 @@ export interface DesktopPickerStatus {
36
36
  supported: boolean;
37
37
  trust: PickerTrustState;
38
38
  profile: DesktopPickerProfileInspection["kind"];
39
+ /** SHA-256 of this process's picker authority, so a CLI can verify the file it is about to trust; null when no authority exists yet. */
40
+ caSha256: string | null;
39
41
  listenerReady: boolean;
40
42
  effective: boolean;
41
43
  reason: DesktopPickerReason;
@@ -107,6 +109,7 @@ function emptyStatus(config: OcxConfig, platform: NodeJS.Platform, reason: Deskt
107
109
  models: 0,
108
110
  snapshotAt: null,
109
111
  lastBootstrapAt: null,
112
+ caSha256: null,
110
113
  };
111
114
  }
112
115
 
@@ -154,6 +157,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
154
157
  models: runtime.models,
155
158
  snapshotAt: runtime.snapshotAt,
156
159
  lastBootstrapAt: runtime.lastBootstrapAt,
160
+ caSha256: publishedPickerCaSha256(deps.configDir),
157
161
  };
158
162
  }
159
163
 
@@ -231,7 +235,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
231
235
  let trust = await inspectPickerTrust(pickerLeafCertPath(deps.configDir), caSha1, deps.security, platform);
232
236
  observedTrust = trust;
233
237
  if (trust !== "trusted" && options.context === "server") {
234
- const added = await trustPickerCa(caPath, deps.security, platform);
238
+ const added = await trustPickerCa(caPath, deps.security, platform, { pem: ca.certPem });
235
239
  trustedByAttempt = added.ok;
236
240
  trust = await inspectPickerTrust(pickerLeafCertPath(deps.configDir), caSha1, deps.security, platform);
237
241
  observedTrust = trust;
@@ -0,0 +1,155 @@
1
+ import type { OcxConfig } from "../types";
2
+ import {
3
+ readWindowsProxyBypassRegistry,
4
+ readWindowsSystemProxy,
5
+ windowsProxyOverrideBypasses,
6
+ type WindowsProxyBypassValues,
7
+ type WindowsSystemProxyResult,
8
+ } from "../lib/windows-system-proxy";
9
+ import { inspectDesktopFirstParty, observeClaudeDesktopMode } from "./desktop-first-party";
10
+ import { desktopFirstPartyDesired } from "./first-party-settings";
11
+
12
+ /**
13
+ * Whether the Windows system proxy silently bypasses Desktop first-party mode.
14
+ *
15
+ * When Claude Desktop starts the Code tab's Claude Code process, it resolves the operating
16
+ * system proxy for the API host and, when that yields an HTTP proxy, passes it to the process
17
+ * as `HTTPS_PROXY`/`HTTP_PROXY`. That inherited value takes precedence over the `env` block
18
+ * OpenCodex writes into the user's `~/.claude/settings.json` (only Claude Code managed settings
19
+ * override it), so Code-tab traffic goes to the system proxy and never reaches the intercept.
20
+ * Routed subagent models then fail, and nothing else reports why. A system proxy such as Clash or
21
+ * v2rayN, with no bypass for the API host, is the common case.
22
+ *
23
+ * Observe-only: this reads the registry and never changes it. A PAC script and WPAD automatic
24
+ * detection decide per request, so they are reported as undecidable rather than guessed, and a
25
+ * failed read is reported as unreadable rather than as an absent value. Like the other doctor
26
+ * proxy surfaces it never prints the proxy value, which can carry credentials.
27
+ */
28
+
29
+ export const FIRST_PARTY_API_HOST = "api.anthropic.com";
30
+
31
+ export type DesktopSystemProxyVerdict = "no-proxy" | "bypassed" | "conflict" | "pac" | "auto-detect" | "unreadable";
32
+
33
+ export type DesktopSystemProxyAssessment =
34
+ | { kind: "not-applicable" }
35
+ | {
36
+ kind: DesktopSystemProxyVerdict;
37
+ /** settings.json carries an owned first-party env that no longer matches the port or token. */
38
+ settingsStale: boolean;
39
+ /** WPAD detection is on (or unknown) next to a static proxy; a WPAD script would win. */
40
+ autoDetectAlsoOn: boolean;
41
+ };
42
+
43
+ export type DesktopFirstPartySettingsState = "applied" | "stale" | "off";
44
+
45
+ export interface DesktopSystemProxyInput {
46
+ platform: NodeJS.Platform;
47
+ firstParty: DesktopFirstPartySettingsState;
48
+ systemProxy: WindowsSystemProxyResult;
49
+ /** `null` when the Internet Settings key could not be read. */
50
+ bypass: WindowsProxyBypassValues | null;
51
+ }
52
+
53
+ function classify(systemProxy: WindowsSystemProxyResult, bypass: WindowsProxyBypassValues | null): DesktopSystemProxyVerdict {
54
+ if (bypass === null || systemProxy.kind === "unreadable") return "unreadable";
55
+ if (bypass.autoConfigUrl) return "pac";
56
+ // Desktop skips SOCKS entries, and an http=-only value does not cover an https:// API host.
57
+ const covering = systemProxy.kind === "proxy" && Boolean(systemProxy.httpsUrl);
58
+ if (!covering) return bypass.autoDetect === false ? "no-proxy" : "auto-detect";
59
+ // A failed WPAD lookup falls back to the static proxy, so a static conflict stands either way.
60
+ return windowsProxyOverrideBypasses(bypass.proxyOverride, FIRST_PARTY_API_HOST) ? "bypassed" : "conflict";
61
+ }
62
+
63
+ export function assessDesktopSystemProxy(input: DesktopSystemProxyInput): DesktopSystemProxyAssessment {
64
+ if (input.platform !== "win32" || input.firstParty === "off") return { kind: "not-applicable" };
65
+ const kind = classify(input.systemProxy, input.bypass);
66
+ return {
67
+ kind,
68
+ settingsStale: input.firstParty === "stale",
69
+ autoDetectAlsoOn: kind === "bypassed" && input.bypass?.autoDetect !== false,
70
+ };
71
+ }
72
+
73
+ const STALE_LINES = [
74
+ " The OpenCodex first-party env in ~/.claude/settings.json is stale (proxy port or token changed);",
75
+ " run `ocx ensure`, then re-run `ocx doctor`.",
76
+ ];
77
+
78
+ function verdictLines(kind: DesktopSystemProxyVerdict): string[] {
79
+ switch (kind) {
80
+ case "no-proxy":
81
+ return [" ok No Windows system proxy covers the Claude API; the Code tab uses the OpenCodex proxy from settings.json."];
82
+ case "bypassed":
83
+ return [` ok ${FIRST_PARTY_API_HOST} is on the Windows proxy bypass list; the Code tab uses the OpenCodex proxy from settings.json.`];
84
+ case "pac":
85
+ return [
86
+ " -- Windows uses a proxy auto-config (PAC) script, so OpenCodex cannot tell whether Claude Desktop",
87
+ ` sends ${FIRST_PARTY_API_HOST} through a proxy. If routed models fail in the Code tab, make the script return DIRECT for it.`,
88
+ ];
89
+ case "auto-detect":
90
+ return [
91
+ " -- Windows automatic proxy detection (WPAD) is on or could not be read, so OpenCodex cannot tell whether",
92
+ ` Claude Desktop sends ${FIRST_PARTY_API_HOST} through a proxy. If routed models fail in the Code tab, turn off`,
93
+ " \"Automatically detect settings\" or make the network's WPAD script return DIRECT for it.",
94
+ ];
95
+ case "unreadable":
96
+ return [" -- Could not read the Windows proxy settings."];
97
+ case "conflict":
98
+ return [
99
+ ` !! The Windows system proxy applies to ${FIRST_PARTY_API_HOST}. Claude Desktop passes it to the Code tab`,
100
+ " as HTTPS_PROXY, which takes precedence over the OpenCodex proxy in ~/.claude/settings.json,",
101
+ " so first-party routing is bypassed and routed models fail there.",
102
+ ` Fix: add ${FIRST_PARTY_API_HOST} to your proxy client's system-proxy bypass list (Clash Verge: system_proxy_bypass),`,
103
+ " then fully quit and reopen Claude Desktop.",
104
+ ];
105
+ }
106
+ }
107
+
108
+ export function formatDesktopSystemProxyLines(assessment: DesktopSystemProxyAssessment): string[] {
109
+ if (assessment.kind === "not-applicable") return [];
110
+ const clear = assessment.kind === "no-proxy" || assessment.kind === "bypassed";
111
+ // A stale env never earns an `ok`: the Code tab would reach a proxy URL that no longer matches.
112
+ if (clear && assessment.settingsStale) return [` -- ${STALE_LINES[0]!.trimStart()}`, STALE_LINES[1]!];
113
+ const lines = verdictLines(assessment.kind);
114
+ if (assessment.autoDetectAlsoOn) {
115
+ lines.push(" Automatic proxy detection (WPAD) is also on; a WPAD script on this network would decide instead.");
116
+ }
117
+ if (assessment.settingsStale) lines.push(...STALE_LINES);
118
+ return lines;
119
+ }
120
+
121
+ export interface DesktopSystemProxyDeps {
122
+ platform?: NodeJS.Platform;
123
+ firstPartyState?: () => DesktopFirstPartySettingsState;
124
+ readSystemProxy?: () => WindowsSystemProxyResult;
125
+ readBypass?: () => WindowsProxyBypassValues | null;
126
+ }
127
+
128
+ function liveFirstPartyState(
129
+ config: Pick<OcxConfig, "claudeCode" | "clientIntegrations" | "port" | "runtimeRole">,
130
+ ): DesktopFirstPartySettingsState {
131
+ try {
132
+ const settings = inspectDesktopFirstParty(config).settings.kind;
133
+ if (settings !== "applied" && settings !== "stale") return "off";
134
+ return desktopFirstPartyDesired(config, observeClaudeDesktopMode(config)) ? settings : "off";
135
+ } catch { // no-excuse-ok: catch -- unreadable Claude settings are no first-party evidence.
136
+ return "off";
137
+ }
138
+ }
139
+
140
+ /** Reads the live state. Registry reads run only on Windows with a Desktop first-party env. */
141
+ export function collectDesktopSystemProxy(
142
+ config: Pick<OcxConfig, "claudeCode" | "clientIntegrations" | "port" | "runtimeRole">,
143
+ deps: DesktopSystemProxyDeps = {},
144
+ ): DesktopSystemProxyAssessment {
145
+ const platform = deps.platform ?? process.platform;
146
+ if (platform !== "win32") return { kind: "not-applicable" };
147
+ const firstParty = (deps.firstPartyState ?? (() => liveFirstPartyState(config)))();
148
+ if (firstParty === "off") return { kind: "not-applicable" };
149
+ return assessDesktopSystemProxy({
150
+ platform,
151
+ firstParty,
152
+ systemProxy: (deps.readSystemProxy ?? readWindowsSystemProxy)(),
153
+ bypass: (deps.readBypass ?? readWindowsProxyBypassRegistry)(),
154
+ });
155
+ }
@@ -96,7 +96,13 @@ export function effortFromOutputConfig(outputConfig: unknown): string | undefine
96
96
  return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined;
97
97
  }
98
98
 
99
- export function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
99
+ function isFineTunedOpenAiTarget(model: string | undefined): boolean {
100
+ if (!model) return false;
101
+ const separator = model.lastIndexOf("/");
102
+ return model.slice(separator + 1).startsWith("ft:");
103
+ }
104
+
105
+ export function formatFromOutputConfig(outputConfig: unknown, resolvedModel?: string): Rec | undefined {
100
106
  if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined;
101
107
  const format = outputConfig.format;
102
108
  if (
@@ -115,7 +121,9 @@ export function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
115
121
  name: "response",
116
122
  schema: format.schema,
117
123
  // Strict Structured Outputs also needs an object at the root; a root anyOf/oneOf is refused.
118
- strict: format.schema.type === "object" && satisfiesOpenAiStrictSchema(format.schema),
124
+ strict: format.schema.type === "object"
125
+ && !Object.hasOwn(format.schema, "anyOf") && !Object.hasOwn(format.schema, "oneOf")
126
+ && satisfiesOpenAiStrictSchema(format.schema, isFineTunedOpenAiTarget(resolvedModel)),
119
127
  };
120
128
  }
121
129
 
@@ -460,7 +460,7 @@ function translateAnthropicRequest(
460
460
  if (Array.isArray(raw.stop_sequences) && raw.stop_sequences.length > 0) {
461
461
  body.stop = raw.stop_sequences.filter((s): s is string => typeof s === "string");
462
462
  }
463
- const outputConfigFormat = formatFromOutputConfig(raw.output_config);
463
+ const outputConfigFormat = formatFromOutputConfig(raw.output_config, body.model as string);
464
464
  if (outputConfigFormat) body.text = { format: outputConfigFormat };
465
465
  let cacheKeySource: ClaudeCacheKeySource = null;
466
466
  if (isRec(raw.metadata) && typeof raw.metadata.user_id === "string") {
@@ -0,0 +1,31 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ acknowledgePendingPickerCaUntrust,
6
+ pendingPickerCaHasLivePublishedOwner,
7
+ readPendingPickerCaUntrust,
8
+ } from "./picker-ca";
9
+ import { untrustPickerCa, type SecurityRunner } from "./picker-trust";
10
+
11
+ /** Drain exactly the recorded public predecessor; keep its journal on any uncertainty. */
12
+ export async function drainPendingPickerCaUntrust(
13
+ configDir: string,
14
+ security?: SecurityRunner,
15
+ platform?: NodeJS.Platform,
16
+ ): Promise<boolean> {
17
+ const pending = readPendingPickerCaUntrust(configDir);
18
+ if (!pending) return true;
19
+ if (pendingPickerCaHasLivePublishedOwner(configDir, pending)) return false;
20
+
21
+ const privateDir = mkdtempSync(join(tmpdir(), "ocx-picker-untrust-"));
22
+ try {
23
+ const publicCopy = join(privateDir, "ca.pem");
24
+ writeFileSync(publicCopy, pending.certPem, { mode: 0o600 });
25
+ const result = await untrustPickerCa(publicCopy, pending.sha1, security, platform);
26
+ if (!result.ok) return false;
27
+ return acknowledgePendingPickerCaUntrust(configDir, pending, result);
28
+ } finally {
29
+ rmSync(privateDir, { recursive: true, force: true });
30
+ }
31
+ }