@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
@@ -1,5 +1,5 @@
1
- import type { OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboTarget, OcxConfig } from "../types";
2
- import { resolveEffortAtOrBelow } from "../reasoning-effort";
1
+ import type { OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboTarget, OcxConfig } from "../types";
2
+ import { isCodexReasoningEffort, resolveEffortAtOrBelow } from "../reasoning-effort";
3
3
  import { resolveComboId } from "./types";
4
4
 
5
5
  const warnedUnsupportedDefaults = new Set<string>();
@@ -60,22 +60,29 @@ export function concreteComboRequestBody(
60
60
  defaultEffort: OcxComboDefaultEffort | null,
61
61
  targetReasoningEfforts: readonly string[] | undefined,
62
62
  reasoningEffortMode: OcxComboReasoningEffortMode = "strict",
63
+ defaultEffortMode: OcxComboDefaultEffortMode = "fallback",
63
64
  ): Record<string, unknown> {
64
65
  const clone = structuredClone(body) as Record<string, unknown>;
65
66
  clone.model = `${target.provider}/${target.model}`;
67
+ if (defaultEffortMode === "force" && (!defaultEffort || !isCodexReasoningEffort(defaultEffort))) {
68
+ throw new Error("force combo default effort requires a valid defaultEffort");
69
+ }
66
70
  if (targetReasoningEfforts?.length === 0
67
71
  || (reasoningEffortMode === "adaptive" && targetReasoningEfforts === undefined)) {
68
72
  stripUnsupportedReasoningControls(clone);
69
73
  }
70
- if (!defaultEffort) return clone;
74
+ if (!defaultEffort || !isCodexReasoningEffort(defaultEffort)) return clone;
71
75
  const reasoning = clone.reasoning;
72
- const needsDefault = reasoning === undefined || (
73
- reasoning
74
- && typeof reasoning === "object"
75
- && !Array.isArray(reasoning)
76
- && !Object.prototype.hasOwnProperty.call(reasoning, "effort")
77
- );
78
- if (!needsDefault) return clone;
76
+ const reasoningRecord = reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)
77
+ ? reasoning as Record<string, unknown>
78
+ : undefined;
79
+ const hasEffort = reasoningRecord !== undefined
80
+ && Object.prototype.hasOwnProperty.call(reasoningRecord, "effort");
81
+ const callerEffort = reasoningRecord?.effort;
82
+ const validCallerEffort = typeof callerEffort === "string" && isCodexReasoningEffort(callerEffort);
83
+ const needsDefault = reasoning === undefined || (reasoningRecord !== undefined && !hasEffort);
84
+ const shouldForce = defaultEffortMode === "force" && validCallerEffort;
85
+ if (!needsDefault && !shouldForce) return clone;
79
86
  // Picker availability treats an unknown ladder as a wildcard, but runtime
80
87
  // injection stays fail-closed until this concrete target advertises support.
81
88
  //
@@ -1,6 +1,6 @@
1
1
  import { isCodexReasoningEffort } from "../reasoning-effort";
2
2
  import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
3
- import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
3
+ import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
4
4
  import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
5
5
 
6
6
  export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
@@ -26,6 +26,8 @@ export interface NormalizedComboConfig {
26
26
  cooldownMs?: number;
27
27
  waitForCooldownMs: number;
28
28
  defaultEffort: OcxComboDefaultEffort | null;
29
+ /** Client-precedence policy; `fallback` preserves legacy behavior. */
30
+ defaultEffortMode: OcxComboDefaultEffortMode;
29
31
  /** Picker-ladder derivation policy; `strict` preserves the legacy intersection rule. */
30
32
  reasoningEffortMode: OcxComboReasoningEffortMode;
31
33
  /** Disable image input; `auto` preserves the intersection derived from all targets. */
@@ -167,6 +169,21 @@ export function comboConfigIssues(
167
169
  message: "defaultEffort must be one of: low, medium, high, xhigh, max, ultra",
168
170
  });
169
171
  }
172
+ if (body.defaultEffortMode !== undefined
173
+ && body.defaultEffortMode !== "fallback"
174
+ && body.defaultEffortMode !== "force") {
175
+ issues.push({
176
+ path: ["defaultEffortMode"],
177
+ message: 'defaultEffortMode must be "fallback" or "force"',
178
+ });
179
+ }
180
+ if (body.defaultEffortMode === "force"
181
+ && (typeof body.defaultEffort !== "string" || !isCodexReasoningEffort(body.defaultEffort))) {
182
+ issues.push({
183
+ path: ["defaultEffort"],
184
+ message: "defaultEffort is required when defaultEffortMode is force",
185
+ });
186
+ }
170
187
  if (body.imageInput !== undefined && body.imageInput !== "auto" && body.imageInput !== "disabled") {
171
188
  issues.push({ path: ["imageInput"], message: 'imageInput must be "auto" or "disabled"' });
172
189
  }
@@ -293,12 +310,16 @@ export function comboConfigError(
293
310
  export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
294
311
  const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
295
312
  const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
313
+ const defaultEffort = typeof raw.defaultEffort === "string" && isCodexReasoningEffort(raw.defaultEffort)
314
+ ? raw.defaultEffort
315
+ : null;
296
316
  return {
297
317
  strategy: raw.strategy ?? "failover",
298
318
  stickyLimit: raw.stickyLimit ?? 1,
299
319
  cooldownMs: raw.cooldownMs,
300
320
  waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
301
- defaultEffort: raw.defaultEffort ?? null,
321
+ defaultEffort,
322
+ defaultEffortMode: raw.defaultEffortMode === "force" && defaultEffort !== null ? "force" : "fallback",
302
323
  reasoningEffortMode: raw.reasoningEffortMode === "adaptive" ? "adaptive" : "strict",
303
324
  imageInput: raw.imageInput === "disabled" ? "disabled" : "auto",
304
325
  alias: alias || null,
@@ -17,6 +17,8 @@ import {
17
17
  forgetEphemeralSecretPath,
18
18
  hardenSecretPath,
19
19
  hardenSecretPathAsync,
20
+ reattributeHardenedSecretPath,
21
+ windowsSecretAclReapPendingForPath,
20
22
  } from "../lib/windows-secret-acl";
21
23
  import {
22
24
  renameAtomicFile,
@@ -26,6 +28,26 @@ import { getConfigDir } from "./paths";
26
28
 
27
29
  let atomicSequence = 0;
28
30
 
31
+ /**
32
+ * Whether this writer's Windows ACL branch applies.
33
+ *
34
+ * Deliberately a local override rather than the ACL module's
35
+ * `windowsSecretAclApplies()`. That one is flipped by many unrelated suites
36
+ * through `setPlatformForTests("win32")`, and honouring it here would make every
37
+ * such suite start running icacls against config writes on a POSIX runner. The
38
+ * gate a cost fix needs is one only the cost tests can open.
39
+ */
40
+ let windowsHardeningOverride: boolean | null = null;
41
+
42
+ function windowsHardeningApplies(): boolean {
43
+ return windowsHardeningOverride ?? process.platform === "win32";
44
+ }
45
+
46
+ /** Test seam: drive the Windows hardening branch on a POSIX runner. Null restores. */
47
+ export function setWindowsHardeningForTests(enabled: boolean | null): void {
48
+ windowsHardeningOverride = enabled;
49
+ }
50
+
29
51
  /** Shared process-wide suffix source for config-owned atomic sibling files. */
30
52
  export function nextAtomicTempSequence(): number {
31
53
  return ++atomicSequence;
@@ -114,6 +136,32 @@ function assertPrivateTempDescriptor(path: string, descriptor: number): void {
114
136
  }
115
137
  }
116
138
 
139
+ /**
140
+ * Carry the harden applied to the empty temp across the content write.
141
+ *
142
+ * The temp is hardened before it holds a byte, and then `atomicWriteFile` hardens
143
+ * it again before the rename. The second call used to be a full icacls sequence
144
+ * rather than a memo hit, because the ACL memo's freshness component is
145
+ * `ctimeNs` and libuv reports that from the NTFS ChangeTime, which a data write
146
+ * moves. So every secret write on Windows applied the same three-step ACL twice:
147
+ * `/grant:r`, `/inheritance:r`, `/remove:g`, plus up to three `/findsid` probes,
148
+ * all of it to arrive at the ACL the file already had.
149
+ *
150
+ * It runs after the descriptor closes, not before, because Windows may not have
151
+ * published the new ChangeTime to a path query while the handle is still open;
152
+ * refreshing to a time the next reader will not see would leave the memo missing
153
+ * and change nothing. What licenses the shortcut is the identity assertion the
154
+ * caller makes immediately before the close, which proves this path resolves to
155
+ * the object the ACL was applied to, plus the object comparison inside
156
+ * `reattributeHardenedSecretPath`, which refuses to move the memo to a different
157
+ * object. Nothing is skipped on the strength of the pathname alone, and a
158
+ * refusal costs only the second full harden this is trying to avoid.
159
+ */
160
+ function carryHardenAcrossContentWrite(path: string): void {
161
+ if (!windowsHardeningApplies()) return;
162
+ reattributeHardenedSecretPath(path);
163
+ }
164
+
117
165
  function writePrivateTempFile(
118
166
  path: string,
119
167
  content: string,
@@ -123,16 +171,24 @@ function writePrivateTempFile(
123
171
  const descriptor = openSync(path, "wx", 0o600);
124
172
  onCreated();
125
173
  try {
126
- if (process.platform === "win32") {
174
+ if (windowsHardeningApplies()) {
127
175
  hardenSecretPath(path, { required: true, timeoutMemoKey });
128
- } else {
176
+ }
177
+ // Keyed on the REAL platform, not the override: on a POSIX host the mode is
178
+ // the boundary and `assertPrivateTempDescriptor` demands 0o600, which an
179
+ // ambient umask can otherwise take away from the open above.
180
+ if (process.platform !== "win32") {
129
181
  fchmodSync(descriptor, 0o600);
130
182
  }
131
183
  assertPrivateTempDescriptor(path, descriptor);
132
184
  writeFileSync(descriptor, content, { encoding: "utf-8" });
185
+ // Second assertion, after the content write: the object this path resolves
186
+ // to is still the object the ACL was applied to and the one we just wrote.
187
+ assertPrivateTempDescriptor(path, descriptor);
133
188
  } finally {
134
189
  closeSync(descriptor);
135
190
  }
191
+ carryHardenAcrossContentWrite(path);
136
192
  }
137
193
 
138
194
  async function writePrivateTempFileAsync(
@@ -144,16 +200,19 @@ async function writePrivateTempFileAsync(
144
200
  const descriptor = openSync(path, "wx", 0o600);
145
201
  onCreated();
146
202
  try {
147
- if (process.platform === "win32") {
203
+ if (windowsHardeningApplies()) {
148
204
  await hardenSecretPathAsync(path, { required: true, timeoutMemoKey });
149
- } else {
205
+ }
206
+ if (process.platform !== "win32") {
150
207
  fchmodSync(descriptor, 0o600);
151
208
  }
152
209
  assertPrivateTempDescriptor(path, descriptor);
153
210
  writeFileSync(descriptor, content, { encoding: "utf-8" });
211
+ assertPrivateTempDescriptor(path, descriptor);
154
212
  } finally {
155
213
  closeSync(descriptor);
156
214
  }
215
+ carryHardenAcrossContentWrite(path);
157
216
  }
158
217
 
159
218
  export function atomicWriteFile(
@@ -171,10 +230,15 @@ export function atomicWriteFile(
171
230
  const effective: AtomicWriteIO = io ?? {
172
231
  write: (tempPath, value) => writePrivateTempFile(tempPath, value, path, () => { ownsTemp = true; }),
173
232
  harden: tempPath => {
174
- try { chmodSync(tempPath, 0o600); } catch { /* platform may ignore chmod */ }
175
- if (process.platform === "win32") {
233
+ // No chmod on the Windows branch: there it sets the read-only ATTRIBUTE,
234
+ // which is not the secret boundary and is not what protects this file, and
235
+ // its ChangeTime bump is what used to invalidate the harden memo one line
236
+ // later. On POSIX the mode IS the boundary, so it stays.
237
+ if (windowsHardeningApplies()) {
176
238
  hardenSecretPath(tempPath, { required: true, timeoutMemoKey: path });
239
+ return;
177
240
  }
241
+ try { chmodSync(tempPath, 0o600); } catch { /* platform may ignore chmod */ }
178
242
  },
179
243
  rename: renameAtomicFile,
180
244
  truncate: tempPath => truncateSync(tempPath, 0),
@@ -245,10 +309,12 @@ export async function atomicWriteFileAsync(
245
309
  const effective: AtomicWriteAsyncIO = io ?? {
246
310
  write: (tempPath, value) => writePrivateTempFileAsync(tempPath, value, path, () => { ownsTemp = true; }),
247
311
  harden: async tempPath => {
248
- try { chmodSync(tempPath, 0o600); } catch { /* platform may ignore chmod */ }
249
- if (process.platform === "win32") {
312
+ // Same reasoning as the synchronous writer above.
313
+ if (windowsHardeningApplies()) {
250
314
  await hardenSecretPathAsync(tempPath, { required: true, timeoutMemoKey: path });
315
+ return;
251
316
  }
317
+ try { chmodSync(tempPath, 0o600); } catch { /* platform may ignore chmod */ }
252
318
  },
253
319
  rename: renameAtomicFileAsync,
254
320
  truncate: target => truncateSync(target, 0),
@@ -258,9 +324,11 @@ export async function atomicWriteFileAsync(
258
324
  assertResolvedTargetAllowed(path, target);
259
325
  const tmp = `${target}.ocx.${process.pid}.${nextAtomicTempSequence()}.tmp`;
260
326
  let hardened = false;
327
+ let tempWasHardenedBeforeContent = false;
261
328
  try {
262
329
  if (io) ownsTemp = true;
263
330
  await effective.write(tmp, content);
331
+ tempWasHardenedBeforeContent = io === undefined && windowsHardeningApplies();
264
332
  await testSeam?.afterTempWrite?.(tmp);
265
333
  await effective.harden(tmp);
266
334
  hardened = true;
@@ -268,6 +336,13 @@ export async function atomicWriteFileAsync(
268
336
  forgetEphemeralSecretPath(tmp);
269
337
  } catch (cause) {
270
338
  if (!ownsTemp) throw cause;
339
+ // The async ACL belt bounds the writer, but it is not evidence that icacls released this
340
+ // path. Leave the temp in the existing residual state instead of racing an unlink against a
341
+ // live Windows handle. The default Windows writer hardens before writing secret bytes; a
342
+ // failure inside that initial harden leaves its still-empty temp behind.
343
+ if (windowsSecretAclReapPendingForPath(tmp)) {
344
+ throw new AtomicWriteResidualTempError(tmp, tempWasHardenedBeforeContent, { cause });
345
+ }
271
346
  let scrubbed = false;
272
347
  try {
273
348
  await effective.truncate(tmp);
@@ -202,6 +202,37 @@ export function pendingTeardownOutstanding(): boolean {
202
202
  }
203
203
  }
204
204
 
205
+ /**
206
+ * Are the outstanding obligations EXACTLY the ones this stop chose to keep?
207
+ *
208
+ * `ocx stop` can preserve its own obligations deliberately — the Codex history preflight
209
+ * refuses before anything is restored, so the receipt has to survive for a later stop
210
+ * (#4718). That is safe for an update to continue past, because the stop knows those
211
+ * receipts describe a proxy it just proved down.
212
+ *
213
+ * Nothing else is. A quarantined receipt is waiting on a human, and a receipt belonging
214
+ * to a live owner means another stop is in flight; letting either ride along would turn
215
+ * "we deliberately kept ours" into "we ignored everyone's". So membership is the test,
216
+ * not a count of ours: an unrecognized obligation of any kind answers false and the
217
+ * caller falls back to the ordinary failure code.
218
+ *
219
+ * Quarantined names are included in the scan on purpose. They do not correspond to any
220
+ * nonce this run preserved, so their presence always answers false.
221
+ */
222
+ export function pendingTeardownsAreExactly(nonces: readonly string[]): boolean {
223
+ const expected = new Set(nonces.map(nonce => `${PREFIX}${nonce}${SUFFIX}`));
224
+ let names: string[];
225
+ try {
226
+ names = readdirSync(getConfigDir());
227
+ } catch (error) {
228
+ // A home that does not exist holds nothing, which matches only an empty expectation.
229
+ // Any other scan failure may be hiding an obligation and must not answer "exactly".
230
+ return (error as NodeJS.ErrnoException).code === "ENOENT" && expected.size === 0;
231
+ }
232
+ const found = names.filter(isAnyTeardownObligationFileName);
233
+ return found.length === expected.size && found.every(name => expected.has(name));
234
+ }
235
+
205
236
  /** Paths of quarantined obligations awaiting a human. */
206
237
  export function listQuarantinedTeardowns(): string[] {
207
238
  try {
@@ -59,6 +59,8 @@ import { parseDesktopProfile } from "../../claude/desktop-profile";
59
59
  import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../../lib/app-owned-memory";
60
60
 
61
61
  export const configSchema = z.object({
62
+ codexNativeSteering: z.boolean().optional().catch(false),
63
+ codexNativeInjection: z.boolean().optional().catch(false),
62
64
  port: z.number().int().min(0).max(65535).default(10100),
63
65
  // A malformed hand edit must disable only remote-role behavior, not discard
64
66
  // providers or data-plane keys. Live writes are rejected explicitly below.
@@ -243,6 +243,7 @@ export const providerConfigSchema = z.object({
243
243
  chatCompletionsPath: z.string().min(1).optional(),
244
244
  statelessResponses: z.boolean().optional(),
245
245
  requiresAdjacentResponsesToolResults: z.boolean().optional(),
246
+ requiresPairedResponsesToolResults: z.boolean().optional(),
246
247
  annotateEmptyToolOutputs: z.boolean().optional(),
247
248
  fastWire: fastWireSchema.nullable().optional(),
248
249
  supportsServiceTier: z.boolean().optional(),