@bitkyc08/opencodex 2.56.0 → 2.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
  3. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
  8. package/src/adapters/command-code.ts +12 -3
  9. package/src/adapters/cursor/cursor-errors.ts +15 -0
  10. package/src/adapters/cursor/discovery.ts +65 -1
  11. package/src/adapters/cursor/envelope-echo.ts +8 -2
  12. package/src/adapters/cursor/live-transport.ts +5 -1
  13. package/src/adapters/cursor/protobuf-events.ts +110 -11
  14. package/src/adapters/cursor/protobuf-request.ts +19 -1
  15. package/src/adapters/cursor/text-toolcall.ts +230 -0
  16. package/src/adapters/cursor/thread-continuity.ts +67 -0
  17. package/src/adapters/cursor/types.ts +5 -0
  18. package/src/adapters/cursor.ts +55 -5
  19. package/src/adapters/google-http.ts +38 -13
  20. package/src/adapters/google.ts +7 -7
  21. package/src/adapters/kiro/payload.ts +17 -3
  22. package/src/adapters/kiro/reasoning.ts +70 -7
  23. package/src/adapters/kiro/stream.ts +8 -2
  24. package/src/adapters/kiro/wire.ts +2 -1
  25. package/src/adapters/kiro-events.ts +21 -13
  26. package/src/adapters/mimo-free.ts +32 -17
  27. package/src/adapters/ollama-native.ts +42 -8
  28. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  29. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  30. package/src/adapters/openai-chat.ts +8 -8
  31. package/src/adapters/openai-responses/passthrough.ts +62 -5
  32. package/src/adapters/openai-responses/request-strips.ts +43 -0
  33. package/src/adapters/physical-send.ts +50 -0
  34. package/src/bridge/errors.ts +26 -2
  35. package/src/bridge/response-json.ts +8 -2
  36. package/src/bridge/sse.ts +20 -2
  37. package/src/claude/desktop-profile.ts +66 -9
  38. package/src/claude/outbound.ts +32 -4
  39. package/src/cli/account-main.ts +1 -1
  40. package/src/cli/capabilities.ts +2 -2
  41. package/src/cli/combo.ts +10 -1
  42. package/src/cli/config-command.ts +35 -18
  43. package/src/cli/dispatch.ts +17 -4
  44. package/src/cli/index.ts +92 -7
  45. package/src/cli/registry.ts +2 -1
  46. package/src/cli/system-command.ts +74 -5
  47. package/src/cli/uninstall-client-state.ts +12 -0
  48. package/src/clients/config-export.ts +7 -3
  49. package/src/codex/account-label.ts +14 -3
  50. package/src/codex/account-store.ts +113 -26
  51. package/src/codex/account-usability.ts +21 -0
  52. package/src/codex/auth-api/login-flow.ts +14 -2
  53. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  54. package/src/codex/auth-context.ts +199 -15
  55. package/src/codex/catalog/aggregation.ts +80 -1
  56. package/src/codex/catalog/model-visibility.ts +1 -0
  57. package/src/codex/catalog/remote.ts +30 -0
  58. package/src/codex/catalog/retained-sync.ts +9 -1
  59. package/src/codex/catalog/routed-gather.ts +38 -1
  60. package/src/codex/cli-install-provenance.ts +7 -1
  61. package/src/codex/convergence.ts +7 -2
  62. package/src/codex/desktop-app/types.ts +11 -2
  63. package/src/codex/desktop-app/windows.ts +5 -5
  64. package/src/codex/desktop-switches.ts +145 -0
  65. package/src/codex/history-job.ts +5 -1
  66. package/src/codex/history-provider.ts +33 -4
  67. package/src/codex/history-worker.ts +14 -1
  68. package/src/codex/inject/remove.ts +145 -7
  69. package/src/codex/inject/restore.ts +231 -32
  70. package/src/codex/inject.ts +12 -16
  71. package/src/codex/loopback-target.ts +9 -0
  72. package/src/codex/model-entitlements.ts +152 -15
  73. package/src/codex/native-profile-startup.ts +64 -20
  74. package/src/codex/pool-refresh-backoff.ts +12 -3
  75. package/src/codex/quota-rejection.ts +104 -15
  76. package/src/codex/routing/cache-affinity.ts +70 -0
  77. package/src/codex/routing/cooldown-math.ts +10 -0
  78. package/src/codex/routing/selection.ts +79 -2
  79. package/src/codex/routing/thread-affinity.ts +50 -2
  80. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  81. package/src/codex/routing.ts +29 -49
  82. package/src/codex/warmup.ts +1 -1
  83. package/src/combos/failover.ts +85 -0
  84. package/src/combos/request.ts +17 -10
  85. package/src/combos/types.ts +23 -2
  86. package/src/config/atomic-write.ts +83 -8
  87. package/src/config/pending-teardown.ts +31 -0
  88. package/src/config/schema/config-schema.ts +2 -0
  89. package/src/config/schema/leaf-validators.ts +1 -0
  90. package/src/generated/compatibility-version.json +272 -180
  91. package/src/images/loop.ts +1 -1
  92. package/src/lib/bounded-subprocess.ts +62 -10
  93. package/src/lib/errors.ts +17 -0
  94. package/src/lib/request-execution-budget.ts +147 -21
  95. package/src/lib/spend-reservation-ledger.ts +18 -0
  96. package/src/lib/state-store-registrations.ts +6 -2
  97. package/src/lib/test-home-guard.ts +85 -1
  98. package/src/lib/upstream-retry.ts +77 -10
  99. package/src/lib/windows-elevation.ts +76 -14
  100. package/src/lib/windows-secret-acl.ts +151 -15
  101. package/src/lib/windows-user-principal.ts +5 -1
  102. package/src/oauth/index.ts +2 -2
  103. package/src/oauth/key-providers.ts +2 -2
  104. package/src/providers/derive.ts +6 -0
  105. package/src/providers/kiro-models.ts +4 -3
  106. package/src/providers/label.ts +19 -1
  107. package/src/providers/model-discovery.ts +35 -7
  108. package/src/providers/registry/entries-core.ts +18 -0
  109. package/src/providers/registry/entries-extended.ts +59 -28
  110. package/src/providers/registry/model-seeds.ts +71 -17
  111. package/src/providers/registry/types.ts +9 -0
  112. package/src/responses/reasoning-envelope.ts +6 -3
  113. package/src/responses/spill-store.ts +17 -0
  114. package/src/responses/state/body-policy.ts +25 -0
  115. package/src/responses/state/spill-queue.ts +8 -6
  116. package/src/responses/state.ts +3 -22
  117. package/src/router.ts +4 -0
  118. package/src/routing/identity-domains.ts +21 -14
  119. package/src/routing/probe-lease.ts +103 -1
  120. package/src/server/auth-cors.ts +1 -0
  121. package/src/server/chat-completions.ts +3 -1
  122. package/src/server/chat-native.ts +37 -9
  123. package/src/server/index/live-sideband.ts +37 -1
  124. package/src/server/index/websocket-handler.ts +54 -3
  125. package/src/server/index.ts +5 -5
  126. package/src/server/inspection-tee.ts +107 -0
  127. package/src/server/live.ts +46 -1
  128. package/src/server/management/combo-routes.ts +10 -1
  129. package/src/server/management/config-routes.ts +27 -5
  130. package/src/server/models-capabilities.ts +24 -3
  131. package/src/server/relay-eager.ts +2 -0
  132. package/src/server/relay.ts +14 -19
  133. package/src/server/request-log.ts +127 -3
  134. package/src/server/response-log-body.ts +153 -0
  135. package/src/server/responses/account-change-state.ts +74 -0
  136. package/src/server/responses/adapter-continuation.ts +33 -7
  137. package/src/server/responses/adapter-delivery.ts +5 -11
  138. package/src/server/responses/adapter-dispatch.ts +84 -13
  139. package/src/server/responses/codex-ws-exchange.ts +65 -4
  140. package/src/server/responses/codex-ws-wire.ts +5 -0
  141. package/src/server/responses/collaboration.ts +74 -4
  142. package/src/server/responses/combo-session-recall.ts +68 -8
  143. package/src/server/responses/combo-stream-preflight.ts +68 -5
  144. package/src/server/responses/compact.ts +54 -13
  145. package/src/server/responses/core-auth.ts +2 -0
  146. package/src/server/responses/core-codex-account.ts +51 -3
  147. package/src/server/responses/core-combo.ts +129 -23
  148. package/src/server/responses/core-errors.ts +18 -0
  149. package/src/server/responses/core-options.ts +3 -0
  150. package/src/server/responses/core-replay.ts +105 -32
  151. package/src/server/responses/core.ts +3 -3
  152. package/src/server/responses/encrypted-payload.ts +0 -1
  153. package/src/server/responses/fetch-helpers.ts +4 -1
  154. package/src/server/responses/input-admission.ts +126 -6
  155. package/src/server/responses/native-injection-protocol.ts +42 -0
  156. package/src/server/responses/native-injection-replay.ts +105 -0
  157. package/src/server/responses/native-injection.ts +242 -0
  158. package/src/server/responses/native-response-control.ts +56 -0
  159. package/src/server/responses/native-response-json.ts +14 -0
  160. package/src/server/responses/native-response-output.ts +37 -0
  161. package/src/server/responses/native-steering-log.ts +44 -0
  162. package/src/server/responses/native-steering-policy.ts +49 -0
  163. package/src/server/responses/native-steering-replay.ts +126 -0
  164. package/src/server/responses/native-steering-settings.ts +76 -0
  165. package/src/server/responses/native-steering.ts +400 -0
  166. package/src/server/responses/native-tool-results.ts +130 -0
  167. package/src/server/responses/passthrough-delivery.ts +30 -6
  168. package/src/server/responses/passthrough-dispatch.ts +61 -11
  169. package/src/server/responses/passthrough-error.ts +38 -2
  170. package/src/server/responses/request-prepare.ts +173 -22
  171. package/src/server/responses/request-send-budget.ts +97 -2
  172. package/src/server/responses/request-spend.ts +147 -0
  173. package/src/server/responses/request-transport.ts +62 -3
  174. package/src/server/responses/run-turn-execution.ts +59 -31
  175. package/src/server/responses/sidecar-execution.ts +7 -13
  176. package/src/server/responses/terminal-guard.ts +65 -4
  177. package/src/server/responses/ws-upstream.ts +21 -1
  178. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  179. package/src/server/stop-teardown.ts +8 -1
  180. package/src/server/ws-bridge.ts +16 -1
  181. package/src/service/cli.ts +13 -1
  182. package/src/service/windows-ops.ts +210 -16
  183. package/src/service/windows-scheduler.ts +28 -21
  184. package/src/service.ts +1 -1
  185. package/src/types/config.ts +8 -1
  186. package/src/types/provider.ts +13 -0
  187. package/src/types/request.ts +8 -5
  188. package/src/types/tools.ts +24 -0
  189. package/src/types.ts +2 -0
  190. package/src/update/index.ts +10 -0
  191. package/src/update/stop-contract.d.mts +1 -0
  192. package/src/update/stop-contract.mjs +19 -0
  193. package/src/update/stop-decision.d.mts +1 -1
  194. package/src/update/stop-decision.mjs +12 -3
  195. package/src/usage/log.ts +1 -1
  196. package/src/vision/anthropic-describe.ts +1 -1
  197. package/src/vision/describe.ts +5 -5
  198. package/src/web-search/anthropic-executor.ts +1 -1
  199. package/src/web-search/exa-executor.ts +1 -1
  200. package/src/web-search/executor.ts +1 -1
  201. package/src/web-search/gemini-executor.ts +1 -1
  202. package/src/web-search/loop.ts +1 -1
  203. package/src/web-search/ollama-executor.ts +1 -1
  204. package/src/web-search/parse.ts +67 -14
  205. package/src/web-search/passthrough-bridge.ts +64 -31
  206. package/src/web-search/xai-executor.ts +1 -1
@@ -2,10 +2,10 @@
2
2
  * Retry guard for upstream fetches that die on stale pooled keep-alive sockets.
3
3
  *
4
4
  * chatgpt.com (Cloudflare) closes idle keep-alive connections server-side; Bun's fetch pool
5
- * reuses the half-closed socket and the request write fails with ECONNRESET before any
6
- * response bytes arrive. Retrying on a fresh connection is safe for our replayable
7
- * (string-body) upstream requests, because fetch() rejects only before response headers —
8
- * a caught error here means no response was ever received.
5
+ * reuses the half-closed socket and a request can fail before response headers arrive.
6
+ * A pre-header rejection does not prove that the origin did not process the request.
7
+ * Mechanically reusable bytes do not make a model POST idempotent: an ambiguous reset
8
+ * becomes a terminal, non-replayable response unless the operation is explicitly safe.
9
9
  *
10
10
  * Deliberately narrow: timeouts, aborts, ECONNREFUSED/DNS/TLS failures, and HTTP error
11
11
  * statuses (returned as Response, never thrown) are NOT retried. Mid-stream SSE resets are
@@ -36,19 +36,66 @@ export function isNonReplayableResponse(response: Response): boolean {
36
36
  return nonReplayableResponses.has(response);
37
37
  }
38
38
 
39
+ /**
40
+ * The narrower marker: responses this proxy synthesized as a replay refusal.
41
+ *
42
+ * {@link isNonReplayableResponse} answers "must not be sent again", which the WebSocket
43
+ * post-send verdicts share. This one answers "the upstream never said this", and that is the
44
+ * question a quota recorder or a `Retry-After` synthesizer has to ask. Both were written for
45
+ * a status that only ever arrived from a provider, so a synthetic 429 reads to them as a
46
+ * credential that rate-limited us and as a wait worth honouring -- one writes a cooldown
47
+ * against a credential that refused nothing, the other instructs the client to send the turn
48
+ * again. A marker rather than a body check, because it has to be answerable before the body
49
+ * is read and cannot be spoofed by an upstream that happens to echo the code.
50
+ */
51
+ const replayRefusalResponses = new WeakSet<Response>();
52
+
53
+ export function markReplayRefusalResponse(response: Response): void {
54
+ replayRefusalResponses.add(response);
55
+ }
56
+
57
+ export function isReplayRefusalResponse(response: Response): boolean {
58
+ return replayRefusalResponses.has(response);
59
+ }
60
+
39
61
  /** Origin never produced a response event; the turn may still be executing. */
40
62
  export const UPSTREAM_NO_RESPONSE_CODE = "upstream_no_response";
41
63
  /** Transport closed after the send, before any response event. */
42
64
  export const UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE = "upstream_closed_before_response";
65
+ /**
66
+ * This proxy refused to replay a pre-header fetch rejection.
67
+ *
68
+ * Distinct from {@link UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE}, which the Codex WebSocket
69
+ * transport settles as a 502 after the create frame was already sent. Both are ambiguous,
70
+ * but only this one is a refusal this process made before any response existed, so it
71
+ * follows the send-budget precedent and answers 429: the Codex client is configured with
72
+ * `retry_429: false` and `retry_5xx: true` over four attempts, so a 5xx here multiplies
73
+ * the duplicate send the refusal exists to prevent. See
74
+ * structure/transports/responses.md#ambiguous-connection-reset-replay-boundary.
75
+ */
76
+ export const UPSTREAM_RESET_REPLAY_REFUSED_CODE = "upstream_reset_replay_refused";
43
77
  const NON_REPLAYABLE_UPSTREAM_CODES: ReadonlySet<string> = new Set([
44
78
  UPSTREAM_NO_RESPONSE_CODE,
45
79
  UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
80
+ UPSTREAM_RESET_REPLAY_REFUSED_CODE,
46
81
  ]);
47
82
 
48
83
  export function isNonReplayableUpstreamCode(code: unknown): boolean {
49
84
  return typeof code === "string" && NON_REPLAYABLE_UPSTREAM_CODES.has(code);
50
85
  }
51
86
 
87
+ /**
88
+ * True for the one non-replayable code this proxy owns end to end. The status it carries is
89
+ * a local decision, so a re-wrapping formatter must restate it rather than inherit the
90
+ * caller's upstream-shaped status.
91
+ */
92
+ export function isReplayRefusalCode(code: unknown): boolean {
93
+ return code === UPSTREAM_RESET_REPLAY_REFUSED_CODE;
94
+ }
95
+
96
+ /** Client-facing status for {@link UPSTREAM_RESET_REPLAY_REFUSED_CODE}. */
97
+ export const REPLAY_REFUSED_STATUS = 429;
98
+
52
99
  // 1 initial + 2 retries: the pool may hold more than one stale socket.
53
100
  const RESET_RETRY_MAX_ATTEMPTS = 3;
54
101
  const RESET_RETRY_BASE_DELAY_MS = 150;
@@ -352,6 +399,12 @@ export async function fetchWithAttemptDeadline(
352
399
  }
353
400
 
354
401
  export interface ResetRetryOptions {
402
+ /**
403
+ * Opt in only when repeating this operation cannot duplicate upstream effects.
404
+ * This permits reset retries, not extra sends: attempts and onSendsConsumed still
405
+ * bound and count every physical send. A string body is not replay-safety proof.
406
+ */
407
+ replaySafe?: boolean;
355
408
  abortSignal?: AbortSignal;
356
409
  /** Short host/path label for the retry warn log (no secrets/query strings). */
357
410
  label?: string;
@@ -442,9 +495,9 @@ export function applyUpstreamRecoveryInit<T extends RequestInit>(
442
495
  }
443
496
 
444
497
  /**
445
- * Run `doFetch`, retrying only connection-reset-shaped rejections (see
446
- * isConnectionResetError) with jittered backoff. The caller's thunk must be replay-safe
447
- * (string body); every retry is logged so persistent resets stay visible.
498
+ * Run `doFetch` within one send budget. Connection-reset-shaped rejections are
499
+ * terminal by default; only an explicitly replay-safe operation receives reset retries
500
+ * with jittered backoff. HTTP responses retain the caller's existing retry policy.
448
501
  */
449
502
  export async function fetchWithResetRetry(
450
503
  doFetch: ReplayableFetch,
@@ -475,6 +528,20 @@ export async function fetchWithResetRetry(
475
528
  if (sawReset) throw new UpstreamRetryEvidenceError([], err, true);
476
529
  throw err;
477
530
  }
531
+ if (opts.replaySafe !== true) {
532
+ // Return evidence instead of throwing a generic transport error: outer catches
533
+ // otherwise turn it into a replayable 502 and a combo/account recovery resends it.
534
+ // The WeakSet protects in-process recovery; the code survives JSON re-wrapping.
535
+ // Never expose the raw exception, which can contain credentials or request data.
536
+ const response = new Response(JSON.stringify({ error: {
537
+ type: "upstream_error",
538
+ code: UPSTREAM_RESET_REPLAY_REFUSED_CODE,
539
+ message: "The upstream connection closed before a response was received. The request may already have been processed; automatic replay was stopped.",
540
+ } }), { status: REPLAY_REFUSED_STATUS, headers: { "content-type": "application/json" } });
541
+ markResponseNonReplayable(response);
542
+ markReplayRefusalResponse(response);
543
+ return response;
544
+ }
478
545
  if (attempt === attempts - 1) throw err;
479
546
  sawReset = true;
480
547
  lastError = err;
@@ -491,9 +558,9 @@ export async function fetchWithResetRetry(
491
558
  }
492
559
 
493
560
  /**
494
- * fetchWithResetRetry plus a transient-5xx status retry layer, PRE-STREAM only: a
495
- * returned Response has by definition not been relayed to the client yet, so replaying
496
- * the (string-body) request is safe. The failed attempt's body is cancelled before the
561
+ * fetchWithResetRetry plus the caller-selected transient-5xx policy, PRE-STREAM only.
562
+ * A received HTTP error follows that policy; an ambiguous reset's non-replayable
563
+ * verdict always stops it. The failed attempt's body is cancelled before the
497
564
  * retry; every returned response (ok, non-transient, aborted, slow, exhausted) keeps
498
565
  * its body intact. Honors Retry-After via retryBackoffDelayMs.
499
566
  *
@@ -250,6 +250,21 @@ export const OCX_ELEVATED_PROTOCOL_FAILED = 13;
250
250
  /** Windows ERROR_CANCELLED — reserved for UAC denial; never emitted by the elevated script. */
251
251
  export const OCX_ELEVATED_UAC_CANCELLED = 1223;
252
252
 
253
+ /**
254
+ * The elevated process could not read a staged payload (#4692).
255
+ *
256
+ * `hardenSecretPath` grants the staging account and strips inheritance, so a split-token
257
+ * elevation of the same user reads the file and an elevation answered with a DIFFERENT
258
+ * administrator's credentials does not. The elevated side cannot explain that itself: it
259
+ * runs hidden, so its stderr goes nowhere and only the exit code survives the boundary.
260
+ * Without a code of its own the operator would be told "exit code 1" for a cause that
261
+ * names its own remedy — the same undiagnosable failure this change set exists to remove.
262
+ *
263
+ * Deliberately outside OCX_ELEVATED_PROTOCOL_CODES: that list is the create-and-run
264
+ * transaction's alphabet, and this code belongs to the registration path.
265
+ */
266
+ export const OCX_ELEVATED_STAGING_UNREADABLE = 14;
267
+
253
268
  export const OCX_ELEVATED_PROTOCOL_CODES = [
254
269
  OCX_ELEVATED_SUCCESS,
255
270
  OCX_ELEVATED_CREATE_FAILED,
@@ -645,36 +660,83 @@ export function runWindowsElevated(file: string, args: string[]): Promise<number
645
660
  }
646
661
 
647
662
  /**
648
- * Register one scheduled-task definition without exposing a mutable XML pathname to
649
- * the elevated process. The XML bytes are fixed in the encoded PowerShell command
650
- * before UAC; Register-ScheduledTask receives that string directly after elevation.
663
+ * A task definition staged for the elevated process.
664
+ *
665
+ * The bytes live in a freshly created, ACL-hardened private directory, and the digest is
666
+ * taken over exactly those bytes by the caller that validated them. The elevated script
667
+ * reads the file once, hashes what it read, and refuses unless the digest matches, so a
668
+ * pathname is no longer a promise about content — it is a claim the receiver checks.
669
+ */
670
+ export interface StagedWindowsTaskXml {
671
+ /** Path inside the caller's hardened staging directory. */
672
+ readonly path: string;
673
+ /** Lowercase hex SHA-256 of the staged bytes (UTF-16LE, no BOM). */
674
+ readonly sha256: string;
675
+ }
676
+
677
+ /**
678
+ * Read a staged payload, prove it is the one that was validated, and decode it.
679
+ *
680
+ * One read: the bytes that are hashed are the same array that is decoded and registered.
681
+ * Hashing a path and then reopening it would reintroduce the swap window this check
682
+ * exists to close.
683
+ */
684
+ const READ_STAGED_TASK_XML = "function Read-OcxStagedTaskXml([string]$path, [string]$expectedHash) {"
685
+ // An unreadable payload is a diagnosable condition, not a generic throw: a hidden
686
+ // elevated process has nowhere to print, so the cause has to ride the exit code.
687
+ + " try { $bytes = [IO.File]::ReadAllBytes($path) }"
688
+ + " catch [System.UnauthorizedAccessException] { exit " + OCX_ELEVATED_STAGING_UNREADABLE + " }"
689
+ + " catch [System.Security.SecurityException] { exit " + OCX_ELEVATED_STAGING_UNREADABLE + " };"
690
+ + " $sha = [Security.Cryptography.SHA256]::Create();"
691
+ + " try { $actual = [BitConverter]::ToString($sha.ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() };"
692
+ + " if ($actual -cne $expectedHash) { throw 'Task Scheduler staged payload failed its integrity check.' };"
693
+ + " return [Text.Encoding]::Unicode.GetString($bytes) }";
694
+
695
+ /**
696
+ * Register one scheduled-task definition from staged, digest-verified bytes.
697
+ *
698
+ * The payloads used to be embedded as base64(utf16le) inside an inner PowerShell script
699
+ * that was itself base64(utf16le)-encoded into `-EncodedCommand`. Two layers of base64
700
+ * over UTF-16 cost about 14.2 command-line characters per XML character, and a
701
+ * replacement carries two payloads, so a ~2 KB task definition pushed the outer command
702
+ * past the Windows limit and the spawn failed with ENAMETOOLONG before UAC ever
703
+ * appeared (#4692). On a host where the trigger scope exports as an account name the
704
+ * re-register path runs on every repair, so repair could never succeed.
705
+ *
706
+ * The command now carries two paths and two 64-character digests, so its length no
707
+ * longer depends on the size of the XML at all.
708
+ *
709
+ * The original design goal was "immutable bytes, never a caller-writable pathname".
710
+ * That goal is kept by different means rather than abandoned: the staging directory is
711
+ * private and ACL-hardened, the files are created exclusively so nothing can be waiting
712
+ * at the path, and the digest makes a same-account swap during the UAC prompt fail
713
+ * closed instead of registering something else. An ACL alone could not do that last
714
+ * part, because a process running as the same user has the same SID.
715
+ *
716
+ * The replacement precondition is unchanged: the elevated process still re-queries the
717
+ * live registration and compares it to the captured predecessor before passing -Force.
651
718
  */
652
719
  export function runWindowsElevatedScheduledTaskRegistration(
653
720
  taskName: string,
654
- xml: string,
721
+ xml: StagedWindowsTaskXml,
655
722
  replace = false,
656
- expectedExistingXml?: string,
723
+ expectedExisting?: StagedWindowsTaskXml,
657
724
  ): Promise<number> {
658
- if (replace && !expectedExistingXml?.trim()) {
725
+ if (replace && !expectedExisting) {
659
726
  throw new Error("Elevated Task Scheduler replacement requires a captured existing definition.");
660
727
  }
661
- const xmlBase64 = Buffer.from(xml, "utf16le").toString("base64");
662
- const expectedExistingBase64 = expectedExistingXml === undefined
663
- ? null
664
- : Buffer.from(expectedExistingXml, "utf16le").toString("base64");
665
728
  const powerShellPath = windowsPowerShell();
666
729
  const powerShellDirectory = powerShellPath.replace(/[\\/][^\\/]+$/, "");
667
730
  const scheduledTasksModule = `${powerShellDirectory}\\Modules\\ScheduledTasks\\ScheduledTasks.psd1`;
668
731
  const inner = [
669
732
  `$taskName = ${psSingleQuote(taskName)}`,
670
- `$xmlBase64 = ${psSingleQuote(xmlBase64)}`,
671
- "$xml = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($xmlBase64))",
733
+ READ_STAGED_TASK_XML,
734
+ `$xml = Read-OcxStagedTaskXml ${psSingleQuote(xml.path)} ${psSingleQuote(xml.sha256)}`,
672
735
  `$module = Microsoft.PowerShell.Core\\Import-Module -Name ${psSingleQuote(scheduledTasksModule)} -PassThru -Force -ErrorAction Stop`,
673
736
  "$registerTask = $module.ExportedCommands['Register-ScheduledTask']",
674
737
  "if ($null -eq $registerTask) { throw 'Trusted ScheduledTasks module does not export Register-ScheduledTask.' }",
675
738
  ...(replace ? [
676
- `$expectedBase64 = ${psSingleQuote(expectedExistingBase64!)}`,
677
- "$expectedXml = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($expectedBase64))",
739
+ `$expectedXml = Read-OcxStagedTaskXml ${psSingleQuote(expectedExisting!.path)} ${psSingleQuote(expectedExisting!.sha256)}`,
678
740
  `$schtasks = ${psSingleQuote(resolveTrustedWindowsSchtasksExe())}`,
679
741
  "$currentXml = & $schtasks /query /tn $taskName /xml 2>$null | Out-String",
680
742
  "if ($LASTEXITCODE -ne 0) { throw 'Task Scheduler replacement precondition could not be read.' }",
@@ -30,8 +30,13 @@
30
30
  */
31
31
 
32
32
  import { existsSync, statSync } from "node:fs";
33
+ import { isAbsolute, relative, resolve } from "node:path";
33
34
  import { env, platform } from "node:process";
34
- import { waitForSubprocessExit } from "./bounded-subprocess";
35
+ import {
36
+ SUBPROCESS_KILL_GRACE_MS,
37
+ waitForSubprocessExit,
38
+ type SubprocessDeadlineScheduler,
39
+ } from "./bounded-subprocess";
35
40
  import { resolveTrustedWindowsIcaclsExe } from "./windows-elevation";
36
41
  import {
37
42
  cachedCurrentWindowsIdentity,
@@ -48,17 +53,32 @@ const hardenedPaths = new Map<string, HardenedIdentity>();
48
53
  * that attempt was consumed. Ordinary callers never consume it.
49
54
  */
50
55
  const timedOutPaths = new Map<string, boolean>();
56
+ /** Compatibility slack before the outer belt releases a caller whose killed child has not reaped. */
57
+ const ASYNC_ICACLS_BELT_MARGIN_MS = 250;
58
+ const pendingAsyncIcaclsReaps = new Map<string, Set<Promise<void>>>();
59
+
60
+ const scheduleAsyncIcaclsBelt: SubprocessDeadlineScheduler = (callback, milliseconds) => {
61
+ const timer = setTimeout(callback, milliseconds);
62
+ return () => clearTimeout(timer);
63
+ };
64
+ let asyncIcaclsBeltScheduler: SubprocessDeadlineScheduler = scheduleAsyncIcaclsBelt;
51
65
 
52
66
  /**
53
- * The memo value: `object:freshness` for a file a harden was actually attributed
54
- * to.
67
+ * The memo value: the `object` plus `freshness` of a file a harden was actually
68
+ * attributed to.
55
69
  *
56
70
  * There is deliberately no null member. An observation that cannot be read is
57
71
  * not stored at all — the entry is deleted — because a "recorded as unverifiable"
58
72
  * value was dead code the moment attribution became a before/after comparison,
59
73
  * and a branch nothing can reach is a branch no test can defend.
74
+ *
75
+ * It is the observation itself rather than a joined string so that the two
76
+ * questions stay separately askable after storage. `reattributeHardenedSecretPath`
77
+ * has to compare the object while deliberately ignoring the freshness, and
78
+ * recovering one half out of `dev:ino:ctimeNs` by counting colons would make that
79
+ * comparison depend on a format nothing declares.
60
80
  */
61
- type HardenedIdentity = string;
81
+ type HardenedIdentity = PathObservation;
62
82
 
63
83
  /**
64
84
  * What a stat can tell us about WHICH OBJECT is at a path.
@@ -128,8 +148,8 @@ function observe(targetPath: string): PathObservation | null {
128
148
  }
129
149
  }
130
150
 
131
- function memoValue(seen: PathObservation): HardenedIdentity {
132
- return `${seen.object}:${seen.freshness}`;
151
+ function sameObservation(a: PathObservation, b: PathObservation): boolean {
152
+ return a.object === b.object && a.freshness === b.freshness;
133
153
  }
134
154
 
135
155
  /**
@@ -155,7 +175,7 @@ function memoSatisfied(cache: Map<string, HardenedIdentity>, targetPath: string)
155
175
  // without any ACL work. That needs exact-identity ABA to bite — outside the
156
176
  // proof bound this unit claims — but "the consequence is out of scope" is not a
157
177
  // reason to keep an entry we have just proven does not describe what is there.
158
- if (current === null || memoValue(current) !== remembered) {
178
+ if (current === null || !sameObservation(current, remembered)) {
159
179
  cache.delete(targetPath);
160
180
  return false;
161
181
  }
@@ -203,7 +223,7 @@ function recordHarden(
203
223
  cache.delete(targetPath);
204
224
  return false;
205
225
  }
206
- cache.set(targetPath, memoValue(after));
226
+ cache.set(targetPath, after);
207
227
  return true;
208
228
  }
209
229
 
@@ -339,7 +359,7 @@ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
339
359
  /**
340
360
  * Async icacls runner (#612): yields the event loop while waiting for the child.
341
361
  * Async Subprocess has no exitedDueToTimeout, so the shared settlement helper
342
- * classifies the deadline and abandons a child that does not settle after kill.
362
+ * classifies the deadline and keeps waiting for a killed child to actually exit.
343
363
  */
344
364
  async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
345
365
  const proc = trySpawnIcacls(args);
@@ -359,21 +379,78 @@ async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Prom
359
379
  function awaitAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
360
380
  return new Promise(resolve => {
361
381
  let settled = false;
362
- let timer: ReturnType<typeof setTimeout> | undefined;
382
+ let cancelBelt: (() => void) | undefined;
363
383
  const finish = (result: IcaclsResult): void => {
364
384
  if (settled) return;
365
385
  settled = true;
366
- if (timer !== undefined) clearTimeout(timer);
386
+ cancelBelt?.();
367
387
  resolve(result);
368
388
  };
369
- timer = setTimeout(
370
- () => finish({ success: false, exitCode: null, timedOut: true, stdout: "" }),
371
- Math.max(1, timeoutMs),
389
+ const runner = asyncIcaclsRunner(args, timeoutMs).then(
390
+ result => { finish(result); },
391
+ () => { finish(spawnFailedResult()); },
392
+ );
393
+ // The belt has to outlast the runner it is guarding, or it is not a belt -- it is the
394
+ // deadline. The runner may now legitimately outlive it while a killed child is reaped. The
395
+ // caller is still released, but the target is registered so removal can wait for the distinct
396
+ // handle-release question instead of treating flight settlement as proof that the child died.
397
+ cancelBelt = asyncIcaclsBeltScheduler(
398
+ () => {
399
+ const targetPath = args[0];
400
+ if (targetPath) registerPendingAsyncIcaclsReap(targetPath, runner);
401
+ finish({ success: false, exitCode: null, timedOut: true, stdout: "" });
402
+ },
403
+ Math.max(1, timeoutMs) + SUBPROCESS_KILL_GRACE_MS + ASYNC_ICACLS_BELT_MARGIN_MS,
372
404
  );
373
- void asyncIcaclsRunner(args, timeoutMs).then(finish, () => finish(spawnFailedResult()));
374
405
  });
375
406
  }
376
407
 
408
+ function registerPendingAsyncIcaclsReap(targetPath: string, reap: Promise<void>): void {
409
+ let pending = pendingAsyncIcaclsReaps.get(targetPath);
410
+ if (!pending) {
411
+ pending = new Set();
412
+ pendingAsyncIcaclsReaps.set(targetPath, pending);
413
+ }
414
+ pending.add(reap);
415
+ void reap.finally(() => {
416
+ pending!.delete(reap);
417
+ if (pending!.size === 0) pendingAsyncIcaclsReaps.delete(targetPath);
418
+ });
419
+ }
420
+
421
+ function pathIsAtOrBelow(targetPath: string, rootPath: string): boolean {
422
+ const relativePath = relative(resolve(rootPath), resolve(targetPath));
423
+ return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
424
+ }
425
+
426
+ /** True while an async icacls runner still owns this exact path after its caller's belt fired. */
427
+ export function windowsSecretAclReapPendingForPath(targetPath: string): boolean {
428
+ return (pendingAsyncIcaclsReaps.get(targetPath)?.size ?? 0) > 0;
429
+ }
430
+
431
+ /** Non-blocking removal guard for callers that must refuse rather than wait for a stuck child. */
432
+ export function windowsSecretAclReapPendingAtOrBelow(rootPath: string): boolean {
433
+ return [...pendingAsyncIcaclsReaps.keys()]
434
+ .some(targetPath => pathIsAtOrBelow(targetPath, rootPath));
435
+ }
436
+
437
+ /**
438
+ * Removal barrier for a file or tree that may still be held by a timed-out icacls child.
439
+ *
440
+ * This wait is deliberately separate from ordinary startup and shutdown: a genuinely stuck child
441
+ * must not defeat the caller-facing belt. Code that chooses to remove the target has the stricter
442
+ * contract and must not proceed until every registered runner at or below it has actually reaped.
443
+ */
444
+ export async function flushWindowsSecretAclReapsBeforeRemoval(rootPath: string): Promise<void> {
445
+ while (true) {
446
+ const pending = [...pendingAsyncIcaclsReaps]
447
+ .filter(([targetPath]) => pathIsAtOrBelow(targetPath, rootPath))
448
+ .flatMap(([, reaps]) => [...reaps]);
449
+ if (pending.length === 0) return;
450
+ await Promise.all(pending);
451
+ }
452
+ }
453
+
377
454
  let icaclsRunner: IcaclsRunner = defaultIcaclsRunner;
378
455
  let asyncIcaclsRunner: AsyncIcaclsRunner = defaultAsyncIcaclsRunner;
379
456
  let platformOverride: string | null = null;
@@ -389,6 +466,13 @@ export function setAsyncIcaclsRunnerForTests(runner: AsyncIcaclsRunner | null):
389
466
  asyncIcaclsRunner = runner ?? defaultAsyncIcaclsRunner;
390
467
  }
391
468
 
469
+ /** Test seam: fire the outer caller-facing belt without sleeping. */
470
+ export function setAsyncIcaclsBeltSchedulerForTests(
471
+ scheduler: SubprocessDeadlineScheduler | null,
472
+ ): void {
473
+ asyncIcaclsBeltScheduler = scheduler ?? scheduleAsyncIcaclsBelt;
474
+ }
475
+
392
476
  /**
393
477
  * Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner.
394
478
  *
@@ -423,6 +507,58 @@ export function forgetHardenedSecretPath(targetPath: string): void {
423
507
  hardenedPaths.delete(targetPath);
424
508
  }
425
509
 
510
+ /**
511
+ * Re-attribute an existing file memo to the SAME object after the caller wrote
512
+ * content to it through a descriptor whose identity it verified.
513
+ *
514
+ * This exists because `freshness` is `ctimeNs`, and on Windows libuv reports
515
+ * `st_ctim` from the NTFS ChangeTime, which moves when file DATA is written. An
516
+ * atomic writer therefore invalidated its own memo between the harden that
517
+ * protects the empty temp and the harden before the rename, and paid a second
518
+ * full `/grant:r` + `/inheritance:r` + `/remove:g` sequence to reapply the ACL
519
+ * that was already on the file. Every secret write on Windows paid it twice.
520
+ *
521
+ * Only the freshness moves, and only for an unchanged object: a different object
522
+ * retires the entry instead. A caller must have proven, immediately beforehand,
523
+ * that `targetPath` resolves to the object its own descriptor refers to.
524
+ *
525
+ * The cost of this is worth stating exactly, because `PathObservation` documents
526
+ * that freshness also moves when PERMISSIONS change, and this call cannot tell
527
+ * the two apart. So a DACL change landing between the harden and this call is
528
+ * absorbed instead of forcing a re-harden. That window is the caller's own
529
+ * content write; every permission change after this call still moves ctime
530
+ * again and still misses the memo, so the detection this memo provides is
531
+ * relocated, not removed.
532
+ *
533
+ * What makes the absorbed window acceptable is who can be in it. Once the harden
534
+ * has run, the DACL is an explicit owner-only ACE with inheritance removed, so
535
+ * no other principal can open the file for `WRITE_DAC` at all. The one principal
536
+ * who can still rewrite that DACL is one holding a handle opened BEFORE the
537
+ * harden, and Windows keeps the access granted to an open handle: that principal
538
+ * can equally rewrite the DACL after any later harden, and after the rename, on
539
+ * the same object. A second mutation pass never bounded that capability — it
540
+ * stripped an ACE the holder could immediately re-add — so declining to repeat
541
+ * it removes no guarantee anyone had.
542
+ *
543
+ * Refusal is cheap and safe in either direction: an unmoved memo simply means the
544
+ * caller's next harden runs in full.
545
+ *
546
+ * Returns whether the memo now describes what is at the path.
547
+ */
548
+ export function reattributeHardenedSecretPath(targetPath: string): boolean {
549
+ const remembered = hardenedPaths.get(targetPath);
550
+ if (remembered === undefined) return false;
551
+ const current = observe(targetPath);
552
+ // Unreadable, or a different object: this is exactly the case the memo must
553
+ // not cover. Retire it so the next harden is a real one.
554
+ if (current === null || current.object !== remembered.object) {
555
+ hardenedPaths.delete(targetPath);
556
+ return false;
557
+ }
558
+ hardenedPaths.set(targetPath, current);
559
+ return true;
560
+ }
561
+
426
562
  /**
427
563
  * Ephemeral-path lifecycle release: clears the success memo AND any timeout
428
564
  * memo keyed by THIS TEMP path in both namespaces. Call only after the temp is
@@ -160,7 +160,11 @@ async function defaultAsyncWindowsPrincipalRunner(
160
160
  stderr: "ignore",
161
161
  windowsHide: true,
162
162
  });
163
- const { exitCode, timedOut } = await waitForSubprocessExit(proc, timeoutMs);
163
+ // No kill grace. The grace exists so a dying child releases a path someone is about to
164
+ // remove; this lookup holds no such path, and it runs during `ocx start`, where the composed
165
+ // acceptance cases already measure real startups at up to 38.8s against a bounded watchdog.
166
+ // Paying two extra seconds per timed-out resolution there buys nothing and costs margin.
167
+ const { exitCode, timedOut } = await waitForSubprocessExit(proc, timeoutMs, 0);
164
168
  // `.bytes()` rather than `.text()`, for the same reason as the sync runner above.
165
169
  const stdout: string | Uint8Array = !timedOut && proc.stdout
166
170
  ? await new Response(proc.stdout).bytes().catch(() => new Uint8Array())
@@ -47,7 +47,7 @@ import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
47
47
  import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
48
48
  import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
49
49
  import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
50
- import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
50
+ import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
51
51
  import { resolveProviderTransport } from "../providers/xai-transport";
52
52
  import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
53
53
  import { logOAuthEvent } from "./log";
@@ -1228,7 +1228,7 @@ export function buildModelsRequest(
1228
1228
  return { url: discoveryUrl(`${base}/v1/models?limit=1000`), headers };
1229
1229
  }
1230
1230
  if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
1231
- return { url: discoveryUrl(`${effectiveProvider.baseUrl}/models`), headers };
1231
+ return { url: discoveryUrl(providerModelsUrl(effectiveProvider.baseUrl)), headers };
1232
1232
  }
1233
1233
 
1234
1234
  /**
@@ -1,6 +1,6 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
2
  import { deriveKeyLoginMap, enrichProviderFromRegistry, type DerivedKeyLoginProvider } from "../providers/derive";
3
- import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
3
+ import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
4
4
 
5
5
  /**
6
6
  * API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
@@ -117,7 +117,7 @@ export async function validateApiKey(
117
117
  providerName,
118
118
  configuredProvider,
119
119
  provider.baseUrl,
120
- `${provider.baseUrl}/models`,
120
+ providerModelsUrl(provider.baseUrl),
121
121
  );
122
122
  const res = await fetch(modelsUrl, {
123
123
  headers: { Authorization: `Bearer ${key}` },
@@ -266,6 +266,9 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
266
266
  ...(entry.requiresAdjacentResponsesToolResults !== undefined
267
267
  ? { requiresAdjacentResponsesToolResults: entry.requiresAdjacentResponsesToolResults }
268
268
  : {}),
269
+ ...(entry.requiresPairedResponsesToolResults !== undefined
270
+ ? { requiresPairedResponsesToolResults: entry.requiresPairedResponsesToolResults }
271
+ : {}),
269
272
  ...(entry.annotateEmptyToolOutputs !== undefined
270
273
  ? { annotateEmptyToolOutputs: entry.annotateEmptyToolOutputs }
271
274
  : {}),
@@ -541,6 +544,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
541
544
  if (prov.requiresAdjacentResponsesToolResults === undefined && seed.requiresAdjacentResponsesToolResults !== undefined) {
542
545
  prov.requiresAdjacentResponsesToolResults = seed.requiresAdjacentResponsesToolResults;
543
546
  }
547
+ if (prov.requiresPairedResponsesToolResults === undefined && seed.requiresPairedResponsesToolResults !== undefined) {
548
+ prov.requiresPairedResponsesToolResults = seed.requiresPairedResponsesToolResults;
549
+ }
544
550
  if (prov.annotateEmptyToolOutputs === undefined && seed.annotateEmptyToolOutputs !== undefined) {
545
551
  prov.annotateEmptyToolOutputs = seed.annotateEmptyToolOutputs;
546
552
  }
@@ -47,9 +47,10 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
47
47
 
48
48
  const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
49
49
 
50
- // gpt-5.6-sol and claude-opus-5 send these values through Kiro's verified native effort fields
51
- // (`reasoning.effort` and `output_config.effort` respectively). Other models map them to bounded
52
- // thinking instructions until their native effort support is verified.
50
+ // The GPT-5.6 family (sol/terra/luna) and claude-opus-5 send these values through Kiro's verified
51
+ // native effort fields (`reasoning.effort` for the GPT-5.6 models, `output_config.effort` for
52
+ // claude-opus-5). Other models map them to bounded thinking instructions until their native
53
+ // effort support is verified.
53
54
  export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
54
55
  KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
55
56
  );
@@ -1,10 +1,28 @@
1
- import { CODEX_ACCOUNT_LOG_LABEL_RE, oauthAccountLogLabel } from "../codex/account-label";
1
+ import { CODEX_ACCOUNT_LOG_LABEL_RE, KEY_ACCOUNT_LOG_LABEL_RE, apiKeyAccountLogLabel, oauthAccountLogLabel } from "../codex/account-label";
2
2
  import type { OcxProviderConfig } from "../types";
3
3
 
4
4
  export function canonicalUsageProviderLabel(provider: string): string {
5
5
  return provider === "chatgpt" || provider === "openai-multi" ? "openai" : provider;
6
6
  }
7
7
 
8
+ export function usesApiKeyAccount(provider: Pick<OcxProviderConfig, "authMode" | "_apiKeyAttempt">): boolean {
9
+ return provider.authMode === "key"
10
+ || (provider.authMode === undefined && !!provider._apiKeyAttempt?.reference);
11
+ }
12
+
13
+ /** Key identity comes from the captured selection, before env/keychain resolution. */
14
+ export function stampApiKeyAccountLabel(
15
+ logCtx: { accountLogLabel?: string },
16
+ providerName: string,
17
+ provider: Pick<OcxProviderConfig, "authMode" | "_apiKeyAttempt">,
18
+ ): void {
19
+ if (usesApiKeyAccount(provider)) {
20
+ logCtx.accountLogLabel = apiKeyAccountLogLabel(providerName, provider._apiKeyAttempt);
21
+ } else if (KEY_ACCOUNT_LOG_LABEL_RE.test(logCtx.accountLogLabel ?? "")) {
22
+ delete logCtx.accountLogLabel;
23
+ }
24
+ }
25
+
8
26
  export function baseProviderLabel(provider: string): string {
9
27
  const canonical = canonicalUsageProviderLabel(provider);
10
28
  if (canonical !== provider) return canonical;