@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -0,0 +1,690 @@
1
+ import { chmodSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { win32 } from "node:path";
3
+ import { winswXmlPath } from "../lib/winsw";
4
+ import { hardenSecretPath } from "../lib/windows-secret-acl";
5
+ import { parseBakedListenPort } from "./health";
6
+ import { windowsServiceScriptPath } from "./state";
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, mkdirSync, mkdtempSync, rmdirSync, unlinkSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { getConfigDir } from "../config";
12
+ import { runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError } from "../lib/windows-elevation";
13
+ import { defaultWinswEntry, installWinswService, statusWinswRaw, uninstallWinswService, WINSW_SERVICE_ID, type WinswStatus } from "../lib/winsw";
14
+ import { forgetEphemeralSecretDir, forgetEphemeralSecretPath, hardenSecretDir } from "../lib/windows-secret-acl";
15
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
16
+ import { killWindowsSchedulerWrappers } from "../lib/windows-service-wrappers";
17
+ import { isTestHomeGuardArmed } from "../lib/test-home-guard";
18
+ import { writeServiceApiTokenFile } from "./guards";
19
+ import type { ServiceStopOutcome } from "./orchestration";
20
+ import { TASK, windowsLauncherVbsPath, windowsTaskXmlPath, serviceStatePath, writeServiceInstallState, serviceSourceDir } from "./state";
21
+ import { decodeSchtasksOutput, querySchtasks, schtasks, type WindowsSchedulerTaskProbe, schtasksErrorDetail, probeWindowsSchedulerTask, rollbackWindowsSchedulerTaskOwnedByAttempt } from "./windows-scheduler";
22
+ import { buildWindowsServiceScript, buildWindowsSchtasksCreateArgs, buildWindowsSchtasksCreateArgsForXml, buildWindowsLauncherVbs, resolvedWindowsTaskSid, buildWindowsTaskXmlDocument, windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy } from "./windows-taskxml";
23
+
24
+ /** The `--port <n>` baked into the Task Scheduler wrapper. Windows scheduler backend. */
25
+ export function windowsListenPort(deps: { readScript?: () => string } = {}): number | null {
26
+ return parseBakedListenPort(deps.readScript ?? (() => readFileSync(windowsServiceScriptPath(), "utf8")));
27
+ }
28
+
29
+ /**
30
+ * The `--port <n>` baked into the WinSW XML's `<arguments>`. Windows native backend.
31
+ *
32
+ * Separate from {@link windowsListenPort} rather than one function branching on
33
+ * `readServiceBackend()`: the recorded backend can disagree with what is actually on
34
+ * disk (the `stale` / `backendStateMismatch` cases `deriveWindowsServiceDiagnostic`
35
+ * exists to catch), and a reader that trusted it would then read the wrong file.
36
+ * Each returns null when its own artifact is absent, so the chain needs no branch.
37
+ */
38
+ export function winswListenPort(deps: { readXml?: () => string } = {}): number | null {
39
+ return parseBakedListenPort(deps.readXml ?? (() => readFileSync(winswXmlPath(), "utf8")));
40
+ }
41
+
42
+ /**
43
+ * Write a service definition with owner-only permissions.
44
+ *
45
+ * These files carry the outbound proxy environment (#2107), and a proxy URL routinely
46
+ * carries `user:password`. `writeFileSync` without a mode lands at 0644 under the default
47
+ * umask, so the credential would be world-readable on a shared host. Every other
48
+ * secret-bearing write in this file already uses 0600 — the service API token and the
49
+ * install state — and a service definition holding a proxy credential belongs in the same
50
+ * class.
51
+ *
52
+ * The explicit `chmodSync` is not redundant: `mode` only applies when the file is
53
+ * created, so an install over a definition left at 0644 by an earlier version would keep
54
+ * the loose mode.
55
+ *
56
+ * On Windows the POSIX bits are advisory, so the ACL is the real boundary — and whether it
57
+ * may soft-fail depends on what the definition actually contains. A definition carrying a
58
+ * proxy credential is a secret publication and fails closed like the API token and the
59
+ * install state do; one carrying only paths and a port is not worth refusing an install
60
+ * over, since before #2107 these files had no hardening at all and a failure here would
61
+ * regress a user who has no credential to protect.
62
+ */
63
+ export function writeServiceDefinitionFile(path: string, content: string, encoding: "utf8" | "utf16le"): void {
64
+ writeFileSync(path, content, { encoding, mode: 0o600 });
65
+ try { chmodSync(path, 0o600); } catch { /* superseded by the Windows ACL below */ }
66
+ if (process.platform === "win32") {
67
+ hardenSecretPath(path, { required: definitionCarriesCredential(content) });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Does this service definition embed a credential-bearing proxy URL?
73
+ *
74
+ * Only the userinfo form leaks something: `http://user:pass@host` in any of the four proxy
75
+ * variables. A bare `http://127.0.0.1:7890` is not a secret, and treating it as one would
76
+ * make an icacls stall fail an install that had nothing to protect.
77
+ *
78
+ * The scan is over any URL in the rendered definition rather than over a `KEY=value` shape,
79
+ * because the three formats render differently — systemd writes `Environment="K=V"`, the
80
+ * plist writes `<key>K</key><string>V</string>`, and the Windows wrapper writes
81
+ * `set "K=V"`. Keying on the assignment syntax silently missed the plist.
82
+ */
83
+ export function definitionCarriesCredential(content: string): boolean {
84
+ // A userinfo authority: scheme, then anything that is not a delimiter, then '@'.
85
+ return /[a-z][a-z0-9+.-]*:\/\/[^\s"'<>/@]+@/i.test(content);
86
+ }
87
+
88
+ // ── Windows (Task Scheduler) ──
89
+ /**
90
+ * In-place service-asset write that tolerates the transient EBUSY/EPERM/EACCES Windows
91
+ * throws while the just-ended task's cmd.exe (or an AV scanner) still holds the file.
92
+ */
93
+ function writeServiceAssetWithRetry(path: string, content: string, encoding: "utf8" | "utf16le"): void {
94
+ for (let attempt = 0; ; attempt++) {
95
+ try {
96
+ writeServiceDefinitionFile(path, content, encoding);
97
+ return;
98
+ } catch (err) {
99
+ const code = (err as NodeJS.ErrnoException).code;
100
+ if (attempt >= 2 || (code !== "EBUSY" && code !== "EPERM" && code !== "EACCES")) throw err;
101
+ Bun.sleepSync(150);
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Rewrite on-disk scheduler assets (script/VBS/XML) without itself registering the task.
108
+ * Fresh install creates it afterwards; repair does so only when the live definition is stale.
109
+ */
110
+ export function writeWindowsSchedulerAssets(): void {
111
+ if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
112
+ writeServiceApiTokenFile();
113
+ const script = windowsServiceScriptPath();
114
+ writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
115
+ // UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
116
+ // paths on some WSH/codepage combinations — same contract as the task XML below.
117
+ writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
118
+ writeServiceAssetWithRetry(
119
+ windowsTaskXmlPath(),
120
+ buildWindowsTaskXmlDocument(script, windowsLauncherVbsPath()),
121
+ "utf16le",
122
+ );
123
+ }
124
+
125
+ const WINDOWS_SCHEDULER_STAGE_PREFIX = "opencodex-service-stage-";
126
+
127
+ const ownedWindowsSchedulerStages = new Set<string>();
128
+
129
+ export interface WindowsSchedulerRegistrationStageDeps {
130
+ createStageDir?: () => string;
131
+ hardenDir?: (path: string) => void;
132
+ writeXml?: (path: string, contents: string) => void;
133
+ hardenPath?: (path: string) => void;
134
+ removeStageDir?: (path: string) => void;
135
+ }
136
+
137
+ function cleanupWindowsSchedulerStage(
138
+ stageDir: string,
139
+ xmlPath: string,
140
+ removeStageDir: (path: string) => void,
141
+ ): void {
142
+ let cleanupError: unknown;
143
+ try {
144
+ unlinkSync(xmlPath);
145
+ forgetEphemeralSecretPath(xmlPath);
146
+ } catch (error) {
147
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
148
+ forgetEphemeralSecretPath(xmlPath);
149
+ } else {
150
+ cleanupError = error;
151
+ }
152
+ }
153
+ try {
154
+ removeStageDir(stageDir);
155
+ forgetEphemeralSecretDir(stageDir);
156
+ } catch (error) {
157
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
158
+ forgetEphemeralSecretDir(stageDir);
159
+ } else if (cleanupError) {
160
+ throw new AggregateError([cleanupError, error], "Task Scheduler staging cleanup failed.");
161
+ } else {
162
+ cleanupError = error;
163
+ }
164
+ }
165
+ if (cleanupError) throw cleanupError;
166
+ }
167
+
168
+ export function stageWindowsSchedulerRegistrationXml(
169
+ attemptNonce: string,
170
+ deps: WindowsSchedulerRegistrationStageDeps = {},
171
+ ): string {
172
+ const createStageDir = deps.createStageDir
173
+ ?? (() => mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX)));
174
+ const hardenDir = deps.hardenDir
175
+ ?? ((path: string) => { hardenSecretDir(path, { required: true }); });
176
+ const writeXml = deps.writeXml ?? ((path: string, contents: string) => {
177
+ writeFileSync(path, contents, { encoding: "utf16le", flag: "wx", mode: 0o600 });
178
+ });
179
+ const hardenPath = deps.hardenPath
180
+ ?? ((path: string) => { hardenSecretPath(path, { required: true }); });
181
+ const removeStageDir = deps.removeStageDir
182
+ ?? ((path: string) => { rmdirSync(path); });
183
+
184
+ let stageDir: string | null = null;
185
+ let xmlPath: string | null = null;
186
+ try {
187
+ stageDir = createStageDir();
188
+ try { chmodSync(stageDir, 0o700); } catch { /* required Windows ACL is authoritative */ }
189
+ hardenDir(stageDir);
190
+ xmlPath = join(stageDir, "task.xml");
191
+ // This document points at the canonical launcher but does not publish or rewrite it.
192
+ // The hardened private directory prevents another local account from replacing the
193
+ // document while UAC is pending; the file harden independently proves its identity.
194
+ writeXml(
195
+ xmlPath,
196
+ buildWindowsTaskXmlDocument(
197
+ windowsServiceScriptPath(),
198
+ windowsLauncherVbsPath(),
199
+ attemptNonce,
200
+ resolvedWindowsTaskSid(),
201
+ ),
202
+ );
203
+ hardenPath(xmlPath);
204
+ ownedWindowsSchedulerStages.add(xmlPath);
205
+ return xmlPath;
206
+ } catch (error) {
207
+ if (stageDir) {
208
+ try {
209
+ cleanupWindowsSchedulerStage(stageDir, xmlPath ?? join(stageDir, "task.xml"), removeStageDir);
210
+ } catch (cleanupError) {
211
+ throw new AggregateError(
212
+ [error, cleanupError],
213
+ "Task Scheduler staging failed and its private temporary directory could not be removed.",
214
+ );
215
+ }
216
+ }
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ export function removeWindowsSchedulerRegistrationStage(xmlPath: string): void {
222
+ if (!ownedWindowsSchedulerStages.has(xmlPath)) {
223
+ throw new Error("Refusing to remove an unrecognized Task Scheduler staging path.");
224
+ }
225
+ const stageDir = dirname(xmlPath);
226
+ cleanupWindowsSchedulerStage(
227
+ stageDir,
228
+ xmlPath,
229
+ path => { rmdirSync(path); },
230
+ );
231
+ if (existsSync(stageDir)) {
232
+ throw new Error("The private Task Scheduler staging directory still exists after cleanup.");
233
+ }
234
+ ownedWindowsSchedulerStages.delete(xmlPath);
235
+ }
236
+
237
+ export interface FreshWindowsSchedulerRegistrationDeps {
238
+ create?: (args: string[]) => void;
239
+ elevate?: (
240
+ taskName: string,
241
+ xml: string,
242
+ replace: boolean,
243
+ expectedExistingXml?: string,
244
+ ) => Promise<void>;
245
+ probe?: () => WindowsSchedulerTaskProbe;
246
+ queryXml?: () => string;
247
+ readExistingXml?: () => string;
248
+ rollback?: () => Promise<string | null>;
249
+ }
250
+
251
+ export async function registerFreshWindowsSchedulerTask(
252
+ xmlPath: string,
253
+ attemptNonce: string,
254
+ deps: FreshWindowsSchedulerRegistrationDeps = {},
255
+ expectedExistingXml?: string,
256
+ ): Promise<void> {
257
+ const replace = expectedExistingXml !== undefined;
258
+ const readExistingXml = deps.readExistingXml ?? statusWindowsXml;
259
+ const assertReplacementPrecondition = (): void => {
260
+ if (!replace) return;
261
+ if (!expectedExistingXml?.trim()) {
262
+ throw new Error("Task Scheduler replacement requires a non-empty captured registration.");
263
+ }
264
+ let currentXml = "";
265
+ try {
266
+ currentXml = readExistingXml();
267
+ } catch {
268
+ throw new Error("Task Scheduler replacement was refused because the current registration could not be read.");
269
+ }
270
+ if (!windowsSchedulerRegistrationMatchesSnapshot(currentXml, expectedExistingXml)) {
271
+ throw new Error("Task Scheduler replacement was refused because the current registration changed.");
272
+ }
273
+ };
274
+ assertReplacementPrecondition();
275
+ const args = buildWindowsSchtasksCreateArgsForXml(xmlPath, replace);
276
+ // Capture and validate the exact definition before an access-denied attempt can
277
+ // cross the UAC boundary. The elevated fallback receives these immutable bytes,
278
+ // never the caller-writable staging pathname.
279
+ const expectedXml = decodeSchtasksOutput(readFileSync(xmlPath));
280
+ if (
281
+ !windowsTaskRegistrationHealthy(expectedXml)
282
+ || !windowsTaskRegistrationOwnedByAttempt(expectedXml, attemptNonce)
283
+ ) {
284
+ throw new Error("The staged Task Scheduler registration failed OpenCodex ownership or shape validation.");
285
+ }
286
+ try {
287
+ (deps.create ?? schtasks)(args);
288
+ } catch (error) {
289
+ if (
290
+ !(error instanceof WindowsSchtasksError)
291
+ || error.operation !== "create"
292
+ || error.reason !== "access-denied"
293
+ ) {
294
+ throw error;
295
+ }
296
+ // Register from the captured XML string inside the elevated process. Another
297
+ // same-user process can mutate its own temp files, but cannot change this command.
298
+ // UAC can remain open for an arbitrary amount of time. Recheck the captured predecessor
299
+ // before launch; the elevated helper repeats the same check after consent and before Force.
300
+ assertReplacementPrecondition();
301
+ const elevate = deps.elevate ?? (async (
302
+ taskName: string,
303
+ xml: string,
304
+ replaceCurrent: boolean,
305
+ previousXml?: string,
306
+ ) => {
307
+ const exitCode = await runWindowsElevatedScheduledTaskRegistration(
308
+ taskName,
309
+ xml,
310
+ replaceCurrent,
311
+ previousXml,
312
+ );
313
+ if (exitCode !== 0) throw new Error(`Background service install failed with exit code ${exitCode}.`);
314
+ });
315
+ await elevate(TASK, expectedXml, replace, expectedExistingXml);
316
+ }
317
+
318
+ const rollbackTask = deps.rollback ?? (() => rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK));
319
+ const probe = (deps.probe ?? (() => probeWindowsSchedulerTask(TASK)))();
320
+ if (probe.status === "absent") {
321
+ throw new Error("Task Scheduler reported success, but the new registration is absent; no service cleanup was started.");
322
+ }
323
+ if (probe.status === "unknown") {
324
+ const rollback = await rollbackTask();
325
+ throw new Error(
326
+ `Task Scheduler registration was not verifiably present after create (${probe.detail}).`
327
+ + (rollback ? ` Cleanup also failed: ${rollback}` : " The unverified registration was rolled back."),
328
+ );
329
+ }
330
+
331
+ let registeredXml = "";
332
+ let queryDetail: string | null = null;
333
+ try {
334
+ registeredXml = (deps.queryXml ?? (() => querySchtasks(["/query", "/tn", TASK, "/xml"])))();
335
+ } catch (error) {
336
+ queryDetail = error instanceof Error ? error.message : String(error);
337
+ }
338
+ if (!registeredXml.trim()) {
339
+ const rollback = await rollbackTask();
340
+ throw new Error(
341
+ "Task Scheduler registration was created, but its live XML could not be verified."
342
+ + (queryDetail ? ` Query failed: ${queryDetail}` : " The query returned an empty document.")
343
+ + (rollback ? ` Cleanup also failed: ${rollback}` : " The unverified registration was rolled back."),
344
+ );
345
+ }
346
+ if (
347
+ !windowsTaskRegistrationHealthy(registeredXml)
348
+ || !windowsTaskRegistrationOwnedByAttempt(registeredXml, attemptNonce)
349
+ ) {
350
+ const rollback = await rollbackTask();
351
+ throw new Error(
352
+ "Task Scheduler registration was created but failed the OpenCodex action/trigger or attempt-ownership verification."
353
+ + (rollback ? ` Cleanup also failed: ${rollback}` : " The invalid registration was rolled back."),
354
+ );
355
+ }
356
+ }
357
+
358
+ export function recordWindowsSchedulerOwnership(): boolean {
359
+ // Ownership claiming is deliberately conservative: a legacy non-empty config root
360
+ // without metadata stays unclaimed, but that must not turn a service reinstall into
361
+ // an outage after prepareServiceInstall has stopped the previous manager.
362
+ return recordOwnedConfigPath(getConfigDir(), serviceStatePath());
363
+ }
364
+
365
+ export interface RemoveNativeWindowsServiceDeps {
366
+ status?: () => WinswStatus;
367
+ uninstall?: () => void;
368
+ sleep?: (ms: number) => void;
369
+ settleChecks?: number;
370
+ }
371
+
372
+ export function removeNativeWindowsServiceForScheduler(
373
+ deps: RemoveNativeWindowsServiceDeps = {},
374
+ ): void {
375
+ const uninstall = deps.uninstall ?? uninstallWinswService;
376
+ // The test home cannot contain SCM. A partially mocked scheduler install must inject
377
+ // the native-service mutation too; otherwise it can stop/delete the user's live WinSW
378
+ // registration even though every filesystem path points at the isolated test home.
379
+ if (isTestHomeGuardArmed() && uninstall === uninstallWinswService) {
380
+ throw new Error(
381
+ "refusing to mutate the machine-global Windows native service from an armed test process; "
382
+ + "inject the native-service removal instead of calling the live manager.",
383
+ );
384
+ }
385
+ const status = deps.status ?? statusWinswRaw;
386
+ const sleep = deps.sleep ?? Bun.sleepSync;
387
+ const settleChecks = Math.max(1, deps.settleChecks ?? 20);
388
+ // Transactional backend switch: installing the scheduler backend removes a native
389
+ // service first — two live managers would both respawn the proxy (conflict).
390
+ if (status() !== "nonexistent") {
391
+ console.log("🔁 Removing the native (WinSW) service before installing the Task Scheduler backend...");
392
+ try {
393
+ uninstall();
394
+ } catch (err) {
395
+ throw new Error(`Cannot remove the native service before switching to Task Scheduler: ${err instanceof Error ? err.message : String(err)}. Remove it manually with 'sc delete ${WINSW_SERVICE_ID}' or retry.`);
396
+ }
397
+ for (let check = 0; check < settleChecks; check++) {
398
+ if (status() === "nonexistent") return;
399
+ if (check + 1 < settleChecks) sleep(250);
400
+ }
401
+ throw new Error(`Native service registration could not be re-verified after the removal attempt — aborting switch. Check 'sc.exe query ${WINSW_SERVICE_ID}' and remove it manually if present.`);
402
+ }
403
+ }
404
+
405
+ export function installWindows(): void {
406
+ recordWindowsSchedulerOwnership();
407
+ removeNativeWindowsServiceForScheduler();
408
+ // End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
409
+ // script mid-rewrite runs a torn batch file, and its open handle can fail the write.
410
+ try { stopWindows(); } catch { /* not running */ }
411
+ writeWindowsSchedulerAssets();
412
+ schtasks(buildWindowsSchtasksCreateArgs(windowsServiceScriptPath()));
413
+ schtasks(["/run", "/tn", TASK]);
414
+ writeServiceInstallState("scheduler");
415
+ }
416
+
417
+ /**
418
+ * Re-register an already-installed scheduler task from a freshly staged definition.
419
+ *
420
+ * Reuses the fresh-install staging and registration path, so the same ownership and shape
421
+ * validation applies and an access-denied `schtasks /create` still escalates through the
422
+ * existing elevated fallback. The staged XML is removed on every exit.
423
+ */
424
+ export async function reregisterWindowsSchedulerTask(
425
+ attemptNonce: string,
426
+ expectedExistingXml: string,
427
+ ): Promise<void> {
428
+ const stagedXml = stageWindowsSchedulerRegistrationXml(attemptNonce);
429
+ try {
430
+ await registerFreshWindowsSchedulerTask(stagedXml, attemptNonce, {}, expectedExistingXml);
431
+ } finally {
432
+ removeWindowsSchedulerRegistrationStage(stagedXml);
433
+ }
434
+ }
435
+
436
+ function stageWindowsSchedulerRestoreXml(registeredXml: string): string {
437
+ if (!registeredXml.trim()) {
438
+ throw new Error("Cannot restore an empty Task Scheduler registration.");
439
+ }
440
+ const stageDir = mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX));
441
+ const xmlPath = join(stageDir, "task.xml");
442
+ try {
443
+ try { chmodSync(stageDir, 0o700); } catch { /* required Windows ACL is authoritative */ }
444
+ hardenSecretDir(stageDir, { required: true });
445
+ writeFileSync(
446
+ xmlPath,
447
+ `\uFEFF${registeredXml.replace(/^\uFEFF/, "")}`,
448
+ { encoding: "utf16le", flag: "wx", mode: 0o600 },
449
+ );
450
+ hardenSecretPath(xmlPath, { required: true });
451
+ ownedWindowsSchedulerStages.add(xmlPath);
452
+ return xmlPath;
453
+ } catch (error) {
454
+ try {
455
+ cleanupWindowsSchedulerStage(stageDir, xmlPath, path => { rmdirSync(path); });
456
+ } catch (cleanupError) {
457
+ throw new AggregateError(
458
+ [error, cleanupError],
459
+ "Task Scheduler rollback staging failed and could not be cleaned up.",
460
+ );
461
+ }
462
+ throw error;
463
+ }
464
+ }
465
+
466
+ /** Compare two live scheduler snapshots conservatively without treating formatting as mutation. */
467
+ export function windowsSchedulerRegistrationMatchesSnapshot(currentXml: string, previousXml: string): boolean {
468
+ const normalize = (xml: string) => xml
469
+ .replace(/^\uFEFF/, "")
470
+ .replace(/\r\n?/g, "\n")
471
+ .trim();
472
+ const current = normalize(currentXml);
473
+ const previous = normalize(previousXml);
474
+ return current.length > 0 && previous.length > 0 && current === previous;
475
+ }
476
+
477
+ /**
478
+ * Restore the captured registration only while the fixed task name is still absent.
479
+ *
480
+ * Both publication paths deliberately omit force: another writer appearing after the
481
+ * absence probe must make this operation fail instead of being overwritten. Exact live
482
+ * XML readback is required before the caller may restart the recovered task.
483
+ */
484
+ export async function restoreWindowsSchedulerTaskIfAbsent(registeredXml: string): Promise<void> {
485
+ const before = probeWindowsSchedulerTask(TASK);
486
+ if (before.status !== "absent") {
487
+ throw new Error(before.status === "present"
488
+ ? "A Task Scheduler registration appeared before recovery and was preserved."
489
+ : `Task Scheduler absence could not be re-verified before recovery (${before.detail}).`);
490
+ }
491
+ const stagedXml = stageWindowsSchedulerRestoreXml(registeredXml);
492
+ try {
493
+ const args = buildWindowsSchtasksCreateArgsForXml(stagedXml, false);
494
+ try {
495
+ schtasks(args);
496
+ } catch (error) {
497
+ if (
498
+ !(error instanceof WindowsSchtasksError)
499
+ || error.operation !== "create"
500
+ || error.reason !== "access-denied"
501
+ ) {
502
+ throw error;
503
+ }
504
+ const exitCode = await runWindowsElevatedScheduledTaskRegistration(TASK, registeredXml, false);
505
+ if (exitCode !== 0) {
506
+ throw new Error(`Task Scheduler rollback failed with exit code ${exitCode}.`);
507
+ }
508
+ }
509
+ const recoveredXml = statusWindowsXml();
510
+ if (!windowsSchedulerRegistrationMatchesSnapshot(recoveredXml, registeredXml)) {
511
+ throw new Error("The recovered Task Scheduler registration did not match the captured definition.");
512
+ }
513
+ } finally {
514
+ removeWindowsSchedulerRegistrationStage(stagedXml);
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Opt-in native backend (`ocx service install --native`). Transactional: removes the
520
+ * scheduler backend first; on failure the machine is left with NO service (explicitly
521
+ * reported) — never a silent fallback to the scheduler.
522
+ */
523
+ /** Refuse WinSW when the interactive user is a Microsoft account (SCM cannot authenticate it). */
524
+ export function assertWindowsNativeServiceAccountSupported(): void {
525
+ if (process.platform !== "win32") return;
526
+ const source = readWindowsPrincipalSource();
527
+ if (source?.toLowerCase() === "microsoftaccount") {
528
+ throw new Error(
529
+ "The native (WinSW) service backend cannot run under a Microsoft-account Windows login. "
530
+ + "Keep the Task Scheduler backend (`ocx service install`) or sign in with a local/domain account before `ocx service install --native`.",
531
+ );
532
+ }
533
+ }
534
+
535
+ function readWindowsPrincipalSource(): string | null {
536
+ if (process.platform !== "win32") return null;
537
+ const ps = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
538
+ if (!existsSync(ps)) return null;
539
+ try {
540
+ const out = execFileSync(ps, [
541
+ "-NoLogo",
542
+ "-NoProfile",
543
+ "-NonInteractive",
544
+ "-Command",
545
+ "(Get-LocalUser -Name $env:USERNAME -ErrorAction SilentlyContinue).PrincipalSource",
546
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
547
+ return out || null;
548
+ } catch {
549
+ return null;
550
+ }
551
+ }
552
+
553
+ export async function installWindowsNative(): Promise<void> {
554
+ assertWindowsNativeServiceAccountSupported();
555
+ recordOwnedConfigPath(getConfigDir(), serviceStatePath());
556
+ if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
557
+ writeServiceApiTokenFile();
558
+ let hadScheduler = false;
559
+ try {
560
+ hadScheduler = schtasks(["/query", "/tn", TASK]).includes(TASK);
561
+ } catch { /* task absent */ }
562
+ if (hadScheduler) {
563
+ console.log("🔁 Removing the Task Scheduler backend before installing the native (WinSW) service...");
564
+ try { stopWindows(); } catch { /* not running */ }
565
+ try {
566
+ uninstallWindows();
567
+ } catch (err) {
568
+ throw new Error(`Cannot remove the Task Scheduler backend before switching to native: ${err instanceof Error ? err.message : String(err)}`);
569
+ }
570
+ // Verify removal — schtasks /delete can silently fail if UAC or policy blocks it.
571
+ try {
572
+ if (schtasks(["/query", "/tn", TASK]).includes(TASK)) {
573
+ throw new Error("Task Scheduler backend still present after removal — aborting switch.");
574
+ }
575
+ } catch (e) {
576
+ if (e instanceof Error && e.message.includes("still present")) throw e;
577
+ /* query failure = task absent, which is what we want */
578
+ }
579
+ }
580
+ try {
581
+ await installWinswService(defaultWinswEntry(serviceSourceDir));
582
+ } catch (err) {
583
+ if (hadScheduler) console.error("⚠️ Native install failed AFTER removing the Task Scheduler backend — no service is installed now. Run `ocx service install` to restore the scheduler backend, or retry `--native`.");
584
+ throw err;
585
+ }
586
+ writeServiceInstallState("native");
587
+ }
588
+
589
+ export function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
590
+
591
+ export function isWindowsSchedulerEndBenign(error: unknown): boolean {
592
+ const detail = schtasksErrorDetail(error).toLowerCase();
593
+ return detail.includes("no running instance")
594
+ || detail.includes("not currently running")
595
+ || detail.includes("0x41330");
596
+ }
597
+
598
+ /**
599
+ * End the scheduler task. "Already stopped" is success; other `/end` failures are
600
+ * swallowed so callers can still run tracked-proxy + live-proxy cleanup.
601
+ *
602
+ * Do not key a restart-window wait on `/end` failure: the #764 case is an `/end`
603
+ * that *succeeds* while the wrapper survives and respawns. That verification lives
604
+ * on the stop-verification path (poll across the restart window), not here.
605
+ */
606
+ export function stopWindows(): void {
607
+ try {
608
+ schtasks(["/end", "/tn", TASK]);
609
+ } catch (error) {
610
+ if (isWindowsSchedulerEndBenign(error)) return;
611
+ }
612
+ }
613
+
614
+ /**
615
+ * `stopWindows` for callers that need to know whether it worked.
616
+ *
617
+ * The void form swallows a non-benign `/end` failure, which is right for best-effort
618
+ * teardown and wrong for deciding whether an update may replace files: a scheduler that
619
+ * refused to stop can respawn the proxy on top of a half-written install (#3008).
620
+ */
621
+ export function stopWindowsChecked(): boolean {
622
+ try {
623
+ schtasks(["/end", "/tn", TASK]);
624
+ return true;
625
+ } catch (error) {
626
+ return isWindowsSchedulerEndBenign(error);
627
+ }
628
+ }
629
+
630
+ export function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
631
+
632
+ export function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
633
+
634
+ /**
635
+ * Best-effort termination of surviving Windows scheduler launcher/wrapper processes.
636
+ * `schtasks /end` ends the task instance but often leaves wscript/cmd running the
637
+ * `:loop` batch, which brings the proxy back during a stop or restart.
638
+ *
639
+ * The matching rule — canonical paths of THIS installation, as complete
640
+ * command-line tokens — lives in lib/windows-service-wrappers so the update job
641
+ * cannot drift away from it again.
642
+ */
643
+ export function killWindowsServiceWrapperProcesses(): void {
644
+ killWindowsSchedulerWrappers({
645
+ scriptPath: windowsServiceScriptPath(),
646
+ launcherPath: windowsLauncherVbsPath(),
647
+ });
648
+ }
649
+
650
+ export function uninstallWindows(): void {
651
+ const probe = probeWindowsSchedulerTask(TASK);
652
+ if (probe.status === "present") {
653
+ try {
654
+ schtasks(["/delete", "/tn", TASK, "/f"]);
655
+ } catch (error) {
656
+ throw new Error(`Failed to delete Task Scheduler task ${TASK}: ${error instanceof Error ? error.message : String(error)}`);
657
+ }
658
+ const afterDelete = probeWindowsSchedulerTask(TASK);
659
+ if (afterDelete.status === "present") {
660
+ throw new Error(`Task Scheduler task ${TASK} is still present after delete — refusing to remove service assets. Retry from an elevated shell.`);
661
+ }
662
+ if (afterDelete.status === "unknown") {
663
+ throw new Error(`Task Scheduler task ${TASK} presence could not be verified after delete — refusing to remove service assets.`);
664
+ }
665
+ } else if (probe.status === "unknown") {
666
+ throw new Error(`Task Scheduler task ${TASK} presence could not be verified — refusing to remove service assets.`);
667
+ }
668
+ if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());
669
+ if (existsSync(windowsLauncherVbsPath())) unlinkSync(windowsLauncherVbsPath());
670
+ if (existsSync(windowsTaskXmlPath())) unlinkSync(windowsTaskXmlPath());
671
+ }
672
+
673
+ /**
674
+ * Collapse the Windows backend observations into one outcome.
675
+ *
676
+ * Extracted so the precedence is testable by calling it. The rule that matters: a readable
677
+ * failure outranks an unreadable state, and an unreadable state outranks success — a
678
+ * scheduler we cannot see may still respawn the proxy.
679
+ */
680
+ export function classifyWindowsServiceStop(o: {
681
+ stopped: boolean;
682
+ failed: boolean;
683
+ schedulerStopped: boolean;
684
+ stateUnknown: boolean;
685
+ }): ServiceStopOutcome {
686
+ if (o.failed) return "failed";
687
+ if (o.stateUnknown) return "state-unknown";
688
+ if (o.stopped) return o.schedulerStopped ? "stopped-respawnable" : "stopped";
689
+ return "absent";
690
+ }