@remodex/rmx 1.0.2

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 (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,817 @@
1
+ /**
2
+ * Windows per-user NTFS ACL hardening for secret files and directories.
3
+ *
4
+ * On Windows, `chmod` only controls POSIX-style bits in the ACE list and does NOT remove
5
+ * inherited permissions from other users. Real per-user isolation requires icacls to:
6
+ * 1. Grant the current user full control (icacls path /grant:r "CURRENTUSER:(F)")
7
+ * 2. Disable inheritance (icacls path /inheritance:r)
8
+ * 3. Strip broad explicit grants by SID (Everyone, Users, Authenticated Users)
9
+ *
10
+ * Owner grant MUST precede `/inheritance:r`. That flag is destructive: it drops inherited
11
+ * ACEs immediately. If a later step times out after inheritance is removed but before an
12
+ * explicit owner ACE exists, the temp is left with a protected empty DACL — owned by the
13
+ * current user yet unreadable/ununlinkable until Full Control is restored (issue #596).
14
+ *
15
+ * On non-Windows platforms the helpers fall through to the caller's existing chmod-based
16
+ * behaviour: they return ok:true without invoking any external process.
17
+ *
18
+ * Design:
19
+ * hardenSecretPath(path, { required: false }) — non-fatal read-path mode.
20
+ * Never throws. Returns { ok, diagnostics? }.
21
+ * hardenSecretPath(path, { required: true }) — write-path mode.
22
+ * Throws a sanitized error (no raw path) on every Windows ACL failure,
23
+ * including genuine icacls timeouts. Required secret publication therefore
24
+ * fails closed; only required:false read-path probes may soft-fail.
25
+ * hardenSecretPathAsync / hardenSecretDirAsync — same policy, async icacls
26
+ * runner so the event loop is not held for the child lifetime (#612).
27
+ * HardenOptions.timeoutMemoKey — optional destination-path key for the
28
+ * timeout memo (atomic writers mint unique temps; never a parent directory).
29
+ * hardenSecretDir — same contract for directories.
30
+ */
31
+
32
+ import { existsSync, statSync } from "node:fs";
33
+ import { env, platform } from "node:process";
34
+ import {
35
+ resolveCurrentWindowsPrincipal,
36
+ resolveCurrentWindowsPrincipalAsync,
37
+ setSyntheticWindowsPrincipalForTests,
38
+ } from "./windows-user-principal";
39
+
40
+ const hardenedDirectories = new Map<string, HardenedIdentity>();
41
+ const hardenedPaths = new Map<string, HardenedIdentity>();
42
+ /**
43
+ * Paths whose harden TIMED OUT this process: do not re-stall every loadConfig on them.
44
+ * `false` means one explicitly authorized recovery attempt remains; `true` means
45
+ * that attempt was consumed. Ordinary callers never consume it.
46
+ */
47
+ const timedOutPaths = new Map<string, boolean>();
48
+
49
+ /**
50
+ * The memo value: `object:freshness` for a file a harden was actually attributed
51
+ * to.
52
+ *
53
+ * There is deliberately no null member. An observation that cannot be read is
54
+ * not stored at all — the entry is deleted — because a "recorded as unverifiable"
55
+ * value was dead code the moment attribution became a before/after comparison,
56
+ * and a branch nothing can reach is a branch no test can defend.
57
+ */
58
+ type HardenedIdentity = string;
59
+
60
+ /**
61
+ * What a stat can tell us about WHICH OBJECT is at a path.
62
+ *
63
+ * Two fields, deliberately separated, because conflating them shipped a bug:
64
+ *
65
+ * - `object` — `dev:ino`. Answers "is this the same file". Survives an ACL or
66
+ * permission change, which is exactly what we need across an icacls call.
67
+ * - `freshness` — `ctimeNs`. Answers "has this file's metadata moved since". It
68
+ * distinguishes an unlink/recreate that ext4 gave the same inode back for, and
69
+ * it MOVES when permissions change.
70
+ *
71
+ * The first version used `dev:ino:ctimeNs` for both jobs. Since chmod bumps ctime
72
+ * — probed, `{ctimeChangedByChmod: true}` — and icacls is a permission change, the
73
+ * before/after comparison would have rejected its own successful harden and failed
74
+ * closed on every first harden on Windows. Requiring the identity to be unchanged
75
+ * across an operation whose entire purpose is to change it is not a strict check;
76
+ * it is a broken one.
77
+ */
78
+ interface PathObservation {
79
+ readonly object: string;
80
+ readonly freshness: string;
81
+ }
82
+
83
+ /**
84
+ * Observe which object is at a path, and how fresh it is.
85
+ *
86
+ * `dev:ino` alone is not enough to detect a replacement, which a Linux CI run
87
+ * proved: ext4 reuses the inode of an unlinked file immediately — 100 of 100
88
+ * unlink/recreate cycles produced the SAME `ino`, while macOS reused none in 200
89
+ * and happily reported the earlier fix working. That is why `freshness` exists;
90
+ * `ctimeNs` differed in 100 of 100 of those same cycles.
91
+ *
92
+ * `bigint: true` is used because `ctimeNs` exists only in that variant.
93
+ *
94
+ * Test seam: `setStatForTests` replaces this reader so a test can vary `dev`,
95
+ * `ino`, and `ctimeNs` independently. Mirroring the implementation's string
96
+ * format in test setup proves nothing about which components production uses —
97
+ * an audit removed `dev` and all forty tests still passed.
98
+ *
99
+ * UNVERIFIED: the plain `ino` is reported to be 0 on NTFS while the bigint form
100
+ * carries the file index, and the zero-ino guard exists for that case. Neither
101
+ * Darwin nor Linux CI can confirm it and no pinned-Bun Windows probe has run.
102
+ * It is defensive code, not a demonstrated platform fact.
103
+ */
104
+ type StatReader = (path: string) => { dev: bigint; ino: bigint; ctimeNs: bigint };
105
+
106
+ const defaultStatReader: StatReader = path => {
107
+ const s = statSync(path, { bigint: true });
108
+ return { dev: s.dev, ino: s.ino, ctimeNs: s.ctimeNs };
109
+ };
110
+
111
+ let statReader: StatReader = defaultStatReader;
112
+
113
+ /** Test seam: drive dev / ino / ctime independently. */
114
+ export function setStatForTests(reader: StatReader | null): void {
115
+ statReader = reader ?? defaultStatReader;
116
+ }
117
+
118
+ function observe(targetPath: string): PathObservation | null {
119
+ try {
120
+ const s = statReader(targetPath);
121
+ if (s.ino === 0n) return null;
122
+ return { object: `${s.dev}:${s.ino}`, freshness: `${s.ctimeNs}` };
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ function memoValue(seen: PathObservation): HardenedIdentity {
129
+ return `${seen.object}:${seen.freshness}`;
130
+ }
131
+
132
+ /**
133
+ * True only when this exact FILE was hardened, not merely this pathname.
134
+ *
135
+ * The memo used to be a `Set<string>` of paths. A stable destination — such as
136
+ * the coordinator database `hardenStableLockFile` hardens — can be unlinked and
137
+ * recreated at the same name, and the replacement inherited the previous file's
138
+ * hardening while never having been through icacls. Ephemeral temps escaped this
139
+ * only because atomic writers call `forgetEphemeralSecretPath` once the temp is
140
+ * gone; nothing does that for a stable path.
141
+ */
142
+ function memoSatisfied(cache: Map<string, HardenedIdentity>, targetPath: string): boolean {
143
+ const remembered = cache.get(targetPath);
144
+ if (remembered === undefined) return false;
145
+ const current = observe(targetPath);
146
+ // Unreadable now is not "unchanged": re-harden rather than trust a value we
147
+ // cannot confirm still describes what is there.
148
+ //
149
+ // A miss RETIRES the entry rather than leaving it. Keeping it left the cache in
150
+ // a state nothing could justify: after a mismatch and a failed re-harden, the
151
+ // stale value survived, so restoring the old identity would satisfy it again
152
+ // without any ACL work. That needs exact-identity ABA to bite — outside the
153
+ // proof bound this unit claims — but "the consequence is out of scope" is not a
154
+ // reason to keep an entry we have just proven does not describe what is there.
155
+ if (current === null || memoValue(current) !== remembered) {
156
+ cache.delete(targetPath);
157
+ return false;
158
+ }
159
+ return true;
160
+ }
161
+
162
+ /**
163
+ * Record a harden ONLY if the file we hardened is still the file at that path.
164
+ *
165
+ * Reading identity after the ACL sequence returns answers "what is there now",
166
+ * which is not the same question as "what did icacls operate on". A replacement
167
+ * landing mid-sequence — probed by swapping the file during the final
168
+ * `/remove:g` — made the memo remember the REPLACEMENT as hardened, so the next
169
+ * acquisition skipped ACL work on a file that had never seen it:
170
+ *
171
+ * {identityChangedDuringHarden: true, callsForOriginal: 3, totalCalls: 3,
172
+ * replacementWasHardened: false}
173
+ *
174
+ * So the OBJECT is captured before the sequence and compared after it. Only the
175
+ * object — `dev:ino` — because icacls changes permissions, and `ctimeNs` moves
176
+ * when permissions change (probed: `{ctimeChangedByChmod: true}`). Comparing the
177
+ * full identity across the call would have rejected every successful harden and
178
+ * failed closed on the first harden on Windows: the check would have been
179
+ * demanding that an operation not do the thing it exists to do.
180
+ *
181
+ * The memo then stores the object plus the freshness read AFTER hardening, which
182
+ * is the state a later lookup should match.
183
+ *
184
+ * A changed object, or an unreadable observation at either end, means we cannot
185
+ * say what was hardened: the memo is cleared rather than written, and required
186
+ * callers fail closed. An optional caller soft-fails, as it does for every other
187
+ * unproven ACL.
188
+ *
189
+ * Returns true when the harden may be reported successful.
190
+ */
191
+ function recordHarden(
192
+ cache: Map<string, HardenedIdentity>,
193
+ targetPath: string,
194
+ before: PathObservation | null,
195
+ ): boolean {
196
+ const after = observe(targetPath);
197
+ if (before === null || after === null || before.object !== after.object) {
198
+ // Never leave a memo behind for a file we cannot vouch for, including one
199
+ // written by an earlier successful harden of a now-replaced file.
200
+ cache.delete(targetPath);
201
+ return false;
202
+ }
203
+ cache.set(targetPath, memoValue(after));
204
+ return true;
205
+ }
206
+
207
+ const SUBSTITUTED_DIAGNOSTIC =
208
+ "ACL hardening could not be attributed — the file at this path changed during hardening";
209
+
210
+ export interface HardenResult {
211
+ ok: boolean;
212
+ diagnostics?: string;
213
+ }
214
+
215
+ export interface HardenOptions {
216
+ required: boolean;
217
+ /**
218
+ * Optional timeout-memo key distinct from `targetPath` (issue #612).
219
+ * Atomic writers mint a fresh `.tmp` path per write; keying the timeout cache by the
220
+ * final destination path prevents re-stalling the event loop on every subsequent temp.
221
+ * Must NOT be a parent directory — directory ACLs are not authoritative for new files.
222
+ */
223
+ timeoutMemoKey?: string;
224
+ /**
225
+ * Consume the one recovery attempt for a previously timed-out memo key.
226
+ * Only a caller that owns its own single-flight and bounded retry policy should
227
+ * set this. It never clears or bypasses an already-consumed timeout memo.
228
+ */
229
+ retryTimedOutOnce?: boolean;
230
+ }
231
+
232
+ /**
233
+ * Total icacls budget per harden call — ALL steps share it, including the single
234
+ * timeout retry and the diagnostic verification pass (no per-attempt fresh budget:
235
+ * loadConfig hardens dir+config+auth sequentially, so per-attempt budgets stack
236
+ * into multi-minute startup stalls). Override with OPENCODEX_ACL_TIMEOUT_MS
237
+ * (integer ms, clamped to [1000, 60000]; invalid values fall back to 30000).
238
+ *
239
+ * The default was 5s until #1156. One envelope has to cover the whole sequence —
240
+ * `/grant:r`, `/inheritance:r`, `/remove:g`, plus the conditional `/findsid`
241
+ * verification — and on machines where icacls is slow (Defender real-time scanning,
242
+ * roaming profiles, a domain-controller round trip) 5s ran out mid-sequence. The
243
+ * harden then failed closed, the native-main owner published a permanent
244
+ * `unavailable`, and every native request returned 503 until restart. A slow start
245
+ * is recoverable; that is not.
246
+ *
247
+ * The cost is honest and worth stating: because loadConfig hardens three paths
248
+ * sequentially, the timeout-path worst case at load is ~90s, and the owner path
249
+ * (initial call + one recovery) is ~60.25s. Both require icacls to be
250
+ * pathologically slow on every call; a healthy machine finishes in milliseconds
251
+ * and sees no change. Operators who prefer the old bound can set the env override.
252
+ */
253
+ const HARDEN_DEADLINE_DEFAULT_MS = 30_000;
254
+ const HARDEN_DEADLINE_MIN_MS = 1_000;
255
+ const HARDEN_DEADLINE_MAX_MS = 60_000;
256
+
257
+ /** Resolve the total harden budget once per call (env mutation cannot change it midway). */
258
+ function resolveHardenDeadlineMs(): number {
259
+ const raw = env["OPENCODEX_ACL_TIMEOUT_MS"]?.trim();
260
+ if (!raw) return HARDEN_DEADLINE_DEFAULT_MS;
261
+ const parsed = Number(raw);
262
+ if (!Number.isSafeInteger(parsed)) return HARDEN_DEADLINE_DEFAULT_MS;
263
+ return Math.min(HARDEN_DEADLINE_MAX_MS, Math.max(HARDEN_DEADLINE_MIN_MS, parsed));
264
+ }
265
+
266
+ export interface IcaclsResult {
267
+ success: boolean;
268
+ exitCode: number | null;
269
+ timedOut: boolean;
270
+ stdout: string;
271
+ }
272
+
273
+ type IcaclsRunner = (args: string[], timeoutMs: number) => IcaclsResult;
274
+ type AsyncIcaclsRunner = (args: string[], timeoutMs: number) => Promise<IcaclsResult>;
275
+
276
+ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
277
+ // Bun.spawnSync with windowsHide: Node execFileSync has hung under the GUI/proxy even
278
+ // with windowsHide, and console-subsystem tools flash a visible window otherwise.
279
+ const result = Bun.spawnSync(["icacls.exe", ...args], {
280
+ stdin: "ignore",
281
+ stdout: "pipe",
282
+ stderr: "ignore",
283
+ timeout: timeoutMs,
284
+ windowsHide: true,
285
+ });
286
+ return {
287
+ success: result.success,
288
+ exitCode: result.exitCode,
289
+ timedOut: result.exitedDueToTimeout ?? false,
290
+ stdout: result.stdout ? result.stdout.toString() : "",
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Async icacls runner (#612): yields the event loop while waiting for the child.
296
+ * Timeout provenance is recorded by our timer (async Subprocess has no exitedDueToTimeout);
297
+ * we still await process exit before classifying so settlement is confirmed.
298
+ */
299
+ async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
300
+ const proc = Bun.spawn(["icacls.exe", ...args], {
301
+ stdin: "ignore",
302
+ stdout: "pipe",
303
+ stderr: "ignore",
304
+ windowsHide: true,
305
+ });
306
+ let timedOutByUs = false;
307
+ const timer = setTimeout(() => {
308
+ timedOutByUs = true;
309
+ try { proc.kill(); } catch { /* already exited */ }
310
+ }, Math.max(1, timeoutMs));
311
+ let exitCode: number | null = null;
312
+ try {
313
+ exitCode = await proc.exited;
314
+ } finally {
315
+ clearTimeout(timer);
316
+ }
317
+ const stdout = proc.stdout
318
+ ? await new Response(proc.stdout).text().catch(() => "")
319
+ : "";
320
+ const timedOut = timedOutByUs;
321
+ return {
322
+ success: !timedOut && exitCode === 0,
323
+ exitCode: timedOut ? null : exitCode,
324
+ timedOut,
325
+ stdout,
326
+ };
327
+ }
328
+
329
+ let icaclsRunner: IcaclsRunner = defaultIcaclsRunner;
330
+ let asyncIcaclsRunner: AsyncIcaclsRunner = defaultAsyncIcaclsRunner;
331
+ let platformOverride: string | null = null;
332
+ let nowFn: () => number = Date.now;
333
+
334
+ /** Test seam: replace the icacls process runner. Pass null to restore the default. */
335
+ export function setIcaclsRunnerForTests(runner: IcaclsRunner | null): void {
336
+ icaclsRunner = runner ?? defaultIcaclsRunner;
337
+ }
338
+
339
+ /** Test seam: replace the async icacls runner. Pass null to restore the default. */
340
+ export function setAsyncIcaclsRunnerForTests(runner: AsyncIcaclsRunner | null): void {
341
+ asyncIcaclsRunner = runner ?? defaultAsyncIcaclsRunner;
342
+ }
343
+
344
+ /**
345
+ * Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner.
346
+ *
347
+ * Faking win32 on a host without System32 also has to supply a principal, or
348
+ * every forced-branch test would fail on the identity lookup instead of
349
+ * exercising icacls. The synthetic value is registered with the resolver, not
350
+ * chosen here, so a test that injects its own runner still wins.
351
+ */
352
+ const SYNTHETIC_TEST_PRINCIPAL = "*S-1-5-21-1-2-3-1001";
353
+
354
+ export function setPlatformForTests(value: string | null): void {
355
+ platformOverride = value;
356
+ setSyntheticWindowsPrincipalForTests(
357
+ value === "win32" && platform !== "win32" ? SYNTHETIC_TEST_PRINCIPAL : null,
358
+ );
359
+ }
360
+
361
+ /** Test seam: injectable clock for deadline tests (no real sleeps). */
362
+ export function setNowForTests(fn: (() => number) | null): void {
363
+ nowFn = fn ?? Date.now;
364
+ }
365
+
366
+ /** Test seam: clear memo/failure caches between cases. */
367
+ export function resetHardenedStateForTests(): void {
368
+ hardenedDirectories.clear();
369
+ hardenedPaths.clear();
370
+ timedOutPaths.clear();
371
+ }
372
+
373
+ /** Forget a successful harden only after this exact ephemeral path is gone. */
374
+ export function forgetHardenedSecretPath(targetPath: string): void {
375
+ hardenedPaths.delete(targetPath);
376
+ }
377
+
378
+ /**
379
+ * Ephemeral-path lifecycle release: clears the success memo AND any timeout
380
+ * memo keyed by THIS TEMP path in both namespaces. Call only after the temp is
381
+ * proven absent (successful rename, successful unlink, ENOENT, or an explicit
382
+ * non-existence check). Never pass a stable destination: destination-keyed
383
+ * timeout memos are intentional anti-restall state and are not touched here.
384
+ */
385
+ export function forgetEphemeralSecretPath(tempPath: string): void {
386
+ hardenedPaths.delete(tempPath);
387
+ timedOutPaths.delete(`required:${tempPath}`);
388
+ timedOutPaths.delete(`optional:${tempPath}`);
389
+ }
390
+
391
+ /** Test seam: timeout memo sets return to baseline after ephemeral cleanup. */
392
+ export function timedOutSecretPathCountForTests(): number {
393
+ return timedOutPaths.size;
394
+ }
395
+
396
+ /** Test seam for proving ephemeral success memos do not grow across replacements. */
397
+ export function hardenedSecretPathCountForTests(): number {
398
+ return hardenedPaths.size;
399
+ }
400
+
401
+ /**
402
+ * Directory counterpart. It had no seam, and that absence hid a real gap: a
403
+ * directory-only pathname memo passed every file-based test in this suite.
404
+ */
405
+ export function hardenedSecretDirCountForTests(): number {
406
+ return hardenedDirectories.size;
407
+ }
408
+
409
+ function effectivePlatform(): string {
410
+ return platformOverride ?? platform;
411
+ }
412
+
413
+ /** Test-aware platform predicate for callers that must avoid even a no-op ACL call. */
414
+ export function windowsSecretAclApplies(): boolean {
415
+ return (platformOverride ?? process.platform) === "win32";
416
+ }
417
+
418
+ /** Error carrying an honest code: ETIMEDOUT only for real timeouts, EICACLS otherwise. */
419
+ function icaclsError(step: string, result: IcaclsResult): NodeJS.ErrnoException {
420
+ const err = new Error(
421
+ result.timedOut ? `icacls ${step} timed out` : `icacls ${step} exited ${result.exitCode ?? "null"}`,
422
+ ) as NodeJS.ErrnoException;
423
+ err.code = result.timedOut ? "ETIMEDOUT" : "EICACLS";
424
+ return err;
425
+ }
426
+
427
+ /**
428
+ * The ACL principal is the effective token SID and nothing else.
429
+ *
430
+ * There is no name-shaped fallback here, and that absence is the fix for #1149
431
+ * rather than an omission. `USERDOMAIN\USERNAME` has the right shape but is not
432
+ * evidence of the current token's subject, and both variables are writable by
433
+ * the process that launched us. Granting Full Control to a wrong principal and
434
+ * then running `/inheritance:r` is destructive in both directions: another
435
+ * account can be left holding the secret, or the file can be left with no ACE
436
+ * the current user can use. When the SID cannot be resolved we decline.
437
+ *
438
+ * Non-Windows hosts that force this branch through `setPlatformForTests` get
439
+ * their principal from `setSyntheticWindowsPrincipalForTests`, which lives with
440
+ * the resolver so an injected runner can still take precedence over it.
441
+ */
442
+ function currentWindowsPrincipal(deadline: number): string {
443
+ return resolveCurrentWindowsPrincipal(deadline - nowFn());
444
+ }
445
+
446
+ async function currentWindowsPrincipalAsync(deadline: number): Promise<string> {
447
+ return resolveCurrentWindowsPrincipalAsync(deadline - nowFn());
448
+ }
449
+
450
+ /**
451
+ * Run icacls to harden a single file system entry.
452
+ * Order is intentional (issue #596): grant the owner ACE first so a later
453
+ * `/inheritance:r` or `/remove:g` timeout cannot strand a protected zero-ACE DACL.
454
+ *
455
+ * We do NOT use a shell string; all arguments are passed as an array so no
456
+ * shell injection is possible even for paths with unusual characters.
457
+ *
458
+ * Throws the raw child_process error on failure (caller sanitizes).
459
+ */
460
+ const BROAD_SIDS = ["*S-1-1-0", "*S-1-5-11", "*S-1-5-32-545"] as const;
461
+
462
+ function grantAce(user: string, directory: boolean): string {
463
+ return directory ? `${user}:(OI)(CI)(F)` : `${user}:(F)`;
464
+ }
465
+
466
+ function runIcacls(targetPath: string, directory: boolean, deadline: number): void {
467
+ const principal = currentWindowsPrincipal(deadline);
468
+
469
+ // The deadline is owned by hardenEntry (total budget incl. retry + verification).
470
+ const run = (step: string, args: string[]): IcaclsResult => {
471
+ const remaining = deadline - nowFn();
472
+ if (remaining <= 0) {
473
+ throw icaclsError(step, { success: false, exitCode: null, timedOut: true, stdout: "" });
474
+ }
475
+ return icaclsRunner(args, remaining);
476
+ };
477
+ const runOrThrow = (step: string, args: string[]): void => {
478
+ const result = run(step, args);
479
+ if (!result.success) throw icaclsError(step, result);
480
+ };
481
+
482
+ // Step 1: grant current user full control BEFORE any destructive ACL change.
483
+ // If this fails, inheritance is untouched and the writer keeps inherited access.
484
+ runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(principal, directory)]);
485
+
486
+ // Step 2: disable inheritance and remove inherited ACEs. The explicit owner ACE
487
+ // from step 1 survives this transition, so a later failure still leaves cleanup access.
488
+ runOrThrow("/inheritance:r", [targetPath, "/inheritance:r"]);
489
+
490
+ // Step 3: remove broad explicit grants using stable SIDs (not localized names).
491
+ // Missing ACEs can yield a non-zero exit; verify with locale-independent /findsid
492
+ // before accepting the failure as harmless — a swallowed real failure would leave
493
+ // Everyone/Users/Authenticated Users grants while reporting hardened.
494
+ // `/remove:g` cannot remove the explicit current-user ACE installed in step 1.
495
+ const removal = run("/remove:g", [targetPath, "/remove:g", ...BROAD_SIDS]);
496
+ if (!removal.success) {
497
+ if (removal.timedOut) throw icaclsError("/remove:g", removal);
498
+ for (const sid of BROAD_SIDS) {
499
+ const found = run("/findsid", [targetPath, "/findsid", sid]);
500
+ if (!found.success) throw icaclsError("/findsid", found);
501
+ // icacls /findsid echoes the target path in its "SID Found" line only when the SID
502
+ // still holds an ACE; the summary lines carry only counts. Matching the path echo —
503
+ // not the (localized) prose — keeps the check locale-independent.
504
+ if (found.stdout.includes(targetPath)) {
505
+ throw icaclsError("/remove:g", removal);
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ /** Async counterpart of runIcacls — same step order and timeout/error classification (#612). */
512
+ async function runIcaclsAsync(targetPath: string, directory: boolean, deadline: number): Promise<void> {
513
+ const principal = await currentWindowsPrincipalAsync(deadline);
514
+
515
+ const run = async (step: string, args: string[]): Promise<IcaclsResult> => {
516
+ const remaining = deadline - nowFn();
517
+ if (remaining <= 0) {
518
+ throw icaclsError(step, { success: false, exitCode: null, timedOut: true, stdout: "" });
519
+ }
520
+ return asyncIcaclsRunner(args, remaining);
521
+ };
522
+ const runOrThrow = async (step: string, args: string[]): Promise<void> => {
523
+ const result = await run(step, args);
524
+ if (!result.success) throw icaclsError(step, result);
525
+ };
526
+
527
+ await runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(principal, directory)]);
528
+ await runOrThrow("/inheritance:r", [targetPath, "/inheritance:r"]);
529
+
530
+ const removal = await run("/remove:g", [targetPath, "/remove:g", ...BROAD_SIDS]);
531
+ if (!removal.success) {
532
+ if (removal.timedOut) throw icaclsError("/remove:g", removal);
533
+ for (const sid of BROAD_SIDS) {
534
+ const found = await run("/findsid", [targetPath, "/findsid", sid]);
535
+ if (!found.success) throw icaclsError("/findsid", found);
536
+ if (found.stdout.includes(targetPath)) {
537
+ throw icaclsError("/remove:g", removal);
538
+ }
539
+ }
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Sanitize an error from a failed ACL operation into a safe diagnostic string.
545
+ * The raw path must not appear in the returned string (it may contain
546
+ * sensitive username components or PII from the home directory path).
547
+ */
548
+ function sanitizeDiagnostics(error: unknown): string {
549
+ // We do not expose the raw error message or any path-like fragments —
550
+ // just an honest, code-specific cause (issue #160: a transient icacls stall
551
+ // must not read like filesystem non-support).
552
+ const code = error instanceof Error && "code" in error ? String((error as NodeJS.ErrnoException).code) : "";
553
+ switch (code) {
554
+ case "ETIMEDOUT":
555
+ return "ACL hardening timed out (ETIMEDOUT) — transient icacls stall; the volume may still support per-user NTFS ACLs";
556
+ case "EPERM":
557
+ case "EACCES":
558
+ return `ACL hardening failed (${code}) — permission denied running icacls`;
559
+ case "EICACLS":
560
+ return "ACL hardening failed (EICACLS) — icacls command error; filesystem may not support per-user NTFS ACLs";
561
+ case "EACLIDENTITY":
562
+ return "ACL hardening failed (EACLIDENTITY) — the effective Windows account SID could not be resolved";
563
+ default:
564
+ return `ACL hardening failed${code ? ` (${code})` : ""} — filesystem may not support per-user NTFS ACLs`;
565
+ }
566
+ }
567
+
568
+ function isTimeoutError(error: unknown): boolean {
569
+ return error instanceof Error && "code" in error
570
+ && String((error as NodeJS.ErrnoException).code) === "ETIMEDOUT";
571
+ }
572
+
573
+ /** Preserve only the bounded machine-readable cause on a sanitized public error. */
574
+ function sanitizedAclError(diagnostics: string, cause: unknown): NodeJS.ErrnoException {
575
+ const error = new Error(diagnostics) as NodeJS.ErrnoException;
576
+ const code = cause && typeof cause === "object" && "code" in cause
577
+ ? String((cause as { code?: unknown }).code)
578
+ : "";
579
+ // EACLIDENTITY belongs here for the same reason as the rest: a caller that
580
+ // catches a required-mode failure has to tell "the SID could not be resolved"
581
+ // apart from "icacls stalled". Without it the code was dropped and only the
582
+ // message carried the cause, which no caller can branch on.
583
+ if (
584
+ code === "ETIMEDOUT" ||
585
+ code === "EICACLS" ||
586
+ code === "EACCES" ||
587
+ code === "EPERM" ||
588
+ code === "EACLIDENTITY"
589
+ ) {
590
+ error.code = code;
591
+ }
592
+ return error;
593
+ }
594
+
595
+ function previousTimeoutError(retryConsumed: boolean): NodeJS.ErrnoException {
596
+ if (retryConsumed) {
597
+ const error = new Error(
598
+ "ACL hardening skipped — the previous timeout recovery was already consumed",
599
+ ) as NodeJS.ErrnoException;
600
+ error.code = "EACLRETRYEXHAUSTED";
601
+ return error;
602
+ }
603
+ return sanitizedAclError(
604
+ "ACL hardening skipped — previous attempt timed out",
605
+ Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }),
606
+ );
607
+ }
608
+
609
+ /** Consume, but never reset, the single explicit recovery attempt for this key. */
610
+ function timeoutMemoErrorIfBlocked(
611
+ memoKey: string,
612
+ opts: HardenOptions,
613
+ ): NodeJS.ErrnoException | null {
614
+ const retryConsumed = timedOutPaths.get(memoKey);
615
+ if (retryConsumed === undefined) return null;
616
+ if (opts.retryTimedOutOnce && retryConsumed === false) {
617
+ timedOutPaths.set(memoKey, true);
618
+ return null;
619
+ }
620
+ return previousTimeoutError(retryConsumed);
621
+ }
622
+
623
+ function recordTimeout(memoKey: string): void {
624
+ if (!timedOutPaths.has(memoKey)) timedOutPaths.set(memoKey, false);
625
+ }
626
+
627
+ /**
628
+ * Diagnostic-only post-timeout probe (never promotes to ok:true — a clean /findsid
629
+ * does not prove inheritance was disabled or the user grant ran; only a fully
630
+ * completed harden sequence may enter the hardened cache). Bounded by the remaining
631
+ * total budget; returns a short state note for the soft-fail diagnostic.
632
+ */
633
+ function describeAclStateAfterTimeout(targetPath: string, deadline: number): string {
634
+ try {
635
+ for (const sid of BROAD_SIDS) {
636
+ const remaining = deadline - nowFn();
637
+ if (remaining <= 0) return "ACL state unverified (budget exhausted)";
638
+ const found = icaclsRunner([targetPath, "/findsid", sid], remaining);
639
+ if (!found.success) return "ACL state unverified (probe failed)";
640
+ if (found.stdout.includes(targetPath)) return "broad ACL grants still present";
641
+ }
642
+ return "no broad ACL grants detected (hardening still incomplete)";
643
+ } catch {
644
+ return "ACL state unverified (probe failed)";
645
+ }
646
+ }
647
+
648
+ async function describeAclStateAfterTimeoutAsync(targetPath: string, deadline: number): Promise<string> {
649
+ try {
650
+ for (const sid of BROAD_SIDS) {
651
+ const remaining = deadline - nowFn();
652
+ if (remaining <= 0) return "ACL state unverified (budget exhausted)";
653
+ const found = await asyncIcaclsRunner([targetPath, "/findsid", sid], remaining);
654
+ if (!found.success) return "ACL state unverified (probe failed)";
655
+ if (found.stdout.includes(targetPath)) return "broad ACL grants still present";
656
+ }
657
+ return "no broad ACL grants detected (hardening still incomplete)";
658
+ } catch {
659
+ return "ACL state unverified (probe failed)";
660
+ }
661
+ }
662
+
663
+ function timeoutMemoKey(targetPath: string, opts: HardenOptions): string {
664
+ // Destination-path memo only (issue #612). Never a parent directory — directory ACLs
665
+ // are not authoritative for newly created temps.
666
+ //
667
+ // Namespace by required-ness (#766): a soft `required:false` timeout during loadConfig
668
+ // must not poison a later `required:true` management-token harden of the same path.
669
+ const base = opts.timeoutMemoKey ?? targetPath;
670
+ return `${opts.required ? "required" : "optional"}:${base}`;
671
+ }
672
+
673
+ /**
674
+ * Shared harden flow for files and directories: one total budget (env-configurable)
675
+ * covering the initial attempt, ONE timeout retry, and the diagnostic verification.
676
+ * Required paths fail closed for every hardening failure, including genuine
677
+ * timeouts. Optional read paths soft-fail with an honest diagnostic.
678
+ */
679
+ function hardenEntry(
680
+ targetPath: string,
681
+ directory: boolean,
682
+ opts: HardenOptions,
683
+ cache: Map<string, HardenedIdentity>,
684
+ ): HardenResult {
685
+ // Observed absence retires the memo. Leaving it would let a later file at this
686
+ // path satisfy the cache if the filesystem ever hands back a matching identity.
687
+ if (!existsSync(targetPath)) { cache.delete(targetPath); return { ok: true }; }
688
+ if (effectivePlatform() !== "win32") return { ok: true };
689
+ if (memoSatisfied(cache, targetPath)) return { ok: true };
690
+ const memoKey = timeoutMemoKey(targetPath, opts);
691
+ const timeoutMemoError = timeoutMemoErrorIfBlocked(memoKey, opts);
692
+ if (timeoutMemoError) {
693
+ if (opts.required) throw timeoutMemoError;
694
+ return { ok: false, diagnostics: timeoutMemoError.message };
695
+ }
696
+
697
+ const deadline = nowFn() + resolveHardenDeadlineMs();
698
+ let lastErr: unknown;
699
+ for (let attempt = 0; attempt < 2; attempt++) {
700
+ if (attempt > 0 && deadline - nowFn() <= 0) break; // retry only while budget remains
701
+ try {
702
+ // Captured BEFORE the sequence: this is the file we are about to harden.
703
+ const before = observe(targetPath);
704
+ runIcacls(targetPath, directory, deadline);
705
+ if (!recordHarden(cache, targetPath, before)) {
706
+ if (opts.required) throw new Error(SUBSTITUTED_DIAGNOSTIC);
707
+ return { ok: false, diagnostics: SUBSTITUTED_DIAGNOSTIC };
708
+ }
709
+ timedOutPaths.delete(memoKey);
710
+ return { ok: true };
711
+ } catch (err) {
712
+ // A substitution is not a transient icacls stall; do not spend the retry on it.
713
+ if (err instanceof Error && err.message === SUBSTITUTED_DIAGNOSTIC) throw err;
714
+ lastErr = err;
715
+ if (!isTimeoutError(err)) break; // real failures do not retry
716
+ }
717
+ }
718
+
719
+ const diagnostics = sanitizeDiagnostics(lastErr);
720
+ if (isTimeoutError(lastErr)) {
721
+ recordTimeout(memoKey);
722
+ const state = describeAclStateAfterTimeout(targetPath, deadline);
723
+ const annotated = `${diagnostics}; ${state}`;
724
+ if (opts.required) throw sanitizedAclError(annotated, lastErr);
725
+ console.warn(`[Remodex] ${annotated} — continuing without NTFS ACL harden`);
726
+ return { ok: false, diagnostics: annotated };
727
+ }
728
+ if (opts.required) throw sanitizedAclError(diagnostics, lastErr);
729
+ return { ok: false, diagnostics };
730
+ }
731
+
732
+ /** Async counterpart of hardenEntry — yields while waiting on icacls (#612). */
733
+ async function hardenEntryAsync(
734
+ targetPath: string,
735
+ directory: boolean,
736
+ opts: HardenOptions,
737
+ cache: Map<string, HardenedIdentity>,
738
+ ): Promise<HardenResult> {
739
+ if (!existsSync(targetPath)) { cache.delete(targetPath); return { ok: true }; }
740
+ if (effectivePlatform() !== "win32") return { ok: true };
741
+ if (memoSatisfied(cache, targetPath)) return { ok: true };
742
+ const memoKey = timeoutMemoKey(targetPath, opts);
743
+ const timeoutMemoError = timeoutMemoErrorIfBlocked(memoKey, opts);
744
+ if (timeoutMemoError) {
745
+ if (opts.required) throw timeoutMemoError;
746
+ return { ok: false, diagnostics: timeoutMemoError.message };
747
+ }
748
+
749
+ const deadline = nowFn() + resolveHardenDeadlineMs();
750
+ let lastErr: unknown;
751
+ for (let attempt = 0; attempt < 2; attempt++) {
752
+ if (attempt > 0 && deadline - nowFn() <= 0) break;
753
+ try {
754
+ const before = observe(targetPath);
755
+ await runIcaclsAsync(targetPath, directory, deadline);
756
+ if (!recordHarden(cache, targetPath, before)) {
757
+ if (opts.required) throw new Error(SUBSTITUTED_DIAGNOSTIC);
758
+ return { ok: false, diagnostics: SUBSTITUTED_DIAGNOSTIC };
759
+ }
760
+ timedOutPaths.delete(memoKey);
761
+ return { ok: true };
762
+ } catch (err) {
763
+ if (err instanceof Error && err.message === SUBSTITUTED_DIAGNOSTIC) throw err;
764
+ lastErr = err;
765
+ if (!isTimeoutError(err)) break;
766
+ }
767
+ }
768
+
769
+ const diagnostics = sanitizeDiagnostics(lastErr);
770
+ if (isTimeoutError(lastErr)) {
771
+ recordTimeout(memoKey);
772
+ const state = await describeAclStateAfterTimeoutAsync(targetPath, deadline);
773
+ const annotated = `${diagnostics}; ${state}`;
774
+ if (opts.required) throw sanitizedAclError(annotated, lastErr);
775
+ console.warn(`[Remodex] ${annotated} — continuing without NTFS ACL harden`);
776
+ return { ok: false, diagnostics: annotated };
777
+ }
778
+ if (opts.required) throw sanitizedAclError(diagnostics, lastErr);
779
+ return { ok: false, diagnostics };
780
+ }
781
+
782
+ /**
783
+ * Harden a single file path with per-user NTFS ACLs on Windows.
784
+ * On non-Windows platforms, returns ok:true immediately (caller owns chmod).
785
+ *
786
+ * @param targetPath Absolute path to the file to harden.
787
+ * @param opts { required: boolean } — required:true throws on failure.
788
+ */
789
+ export function hardenSecretPath(targetPath: string, opts: HardenOptions): HardenResult {
790
+ return hardenEntry(targetPath, false, opts, hardenedPaths);
791
+ }
792
+
793
+ /**
794
+ * Async harden for write paths that must not block the event loop (#612).
795
+ * Same success/timeout/error policy as hardenSecretPath.
796
+ */
797
+ export function hardenSecretPathAsync(targetPath: string, opts: HardenOptions): Promise<HardenResult> {
798
+ return hardenEntryAsync(targetPath, false, opts, hardenedPaths);
799
+ }
800
+
801
+ /**
802
+ * Harden a directory path with per-user NTFS ACLs on Windows.
803
+ * On non-Windows platforms, returns ok:true immediately (caller owns chmod).
804
+ *
805
+ * @param targetPath Absolute path to the directory to harden.
806
+ * @param opts { required: boolean } — required:true throws on failure.
807
+ */
808
+ export function hardenSecretDir(targetPath: string, opts: HardenOptions): HardenResult {
809
+ return hardenEntry(targetPath, true, opts, hardenedDirectories);
810
+ }
811
+
812
+ /**
813
+ * Async directory harden (#612). Same policy as hardenSecretDir.
814
+ */
815
+ export function hardenSecretDirAsync(targetPath: string, opts: HardenOptions): Promise<HardenResult> {
816
+ return hardenEntryAsync(targetPath, true, opts, hardenedDirectories);
817
+ }