@iislee/opencodex 2.11.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -29,13 +29,184 @@
29
29
  * hardenSecretDir — same contract for directories.
30
30
  */
31
31
 
32
- import { existsSync } from "node:fs";
32
+ import { existsSync, statSync } from "node:fs";
33
33
  import { env, platform } from "node:process";
34
+ import { resolveTrustedWindowsIcaclsExe } from "./windows-elevation";
35
+ import {
36
+ resolveCurrentWindowsPrincipal,
37
+ resolveCurrentWindowsPrincipalAsync,
38
+ setSyntheticWindowsPrincipalForTests,
39
+ } from "./windows-user-principal";
40
+
41
+ const hardenedDirectories = new Map<string, HardenedIdentity>();
42
+ const hardenedPaths = new Map<string, HardenedIdentity>();
43
+ /**
44
+ * Paths whose harden TIMED OUT this process: do not re-stall every loadConfig on them.
45
+ * `false` means one explicitly authorized recovery attempt remains; `true` means
46
+ * that attempt was consumed. Ordinary callers never consume it.
47
+ */
48
+ const timedOutPaths = new Map<string, boolean>();
49
+
50
+ /**
51
+ * The memo value: `object:freshness` for a file a harden was actually attributed
52
+ * to.
53
+ *
54
+ * There is deliberately no null member. An observation that cannot be read is
55
+ * not stored at all — the entry is deleted — because a "recorded as unverifiable"
56
+ * value was dead code the moment attribution became a before/after comparison,
57
+ * and a branch nothing can reach is a branch no test can defend.
58
+ */
59
+ type HardenedIdentity = string;
60
+
61
+ /**
62
+ * What a stat can tell us about WHICH OBJECT is at a path.
63
+ *
64
+ * Two fields, deliberately separated, because conflating them shipped a bug:
65
+ *
66
+ * - `object` — `dev:ino`. Answers "is this the same file". Survives an ACL or
67
+ * permission change, which is exactly what we need across an icacls call.
68
+ * - `freshness` — `ctimeNs`. Answers "has this file's metadata moved since". It
69
+ * distinguishes an unlink/recreate that ext4 gave the same inode back for, and
70
+ * it MOVES when permissions change.
71
+ *
72
+ * The first version used `dev:ino:ctimeNs` for both jobs. Since chmod bumps ctime
73
+ * — probed, `{ctimeChangedByChmod: true}` — and icacls is a permission change, the
74
+ * before/after comparison would have rejected its own successful harden and failed
75
+ * closed on every first harden on Windows. Requiring the identity to be unchanged
76
+ * across an operation whose entire purpose is to change it is not a strict check;
77
+ * it is a broken one.
78
+ */
79
+ interface PathObservation {
80
+ readonly object: string;
81
+ readonly freshness: string;
82
+ }
83
+
84
+ /**
85
+ * Observe which object is at a path, and how fresh it is.
86
+ *
87
+ * `dev:ino` alone is not enough to detect a replacement, which a Linux CI run
88
+ * proved: ext4 reuses the inode of an unlinked file immediately — 100 of 100
89
+ * unlink/recreate cycles produced the SAME `ino`, while macOS reused none in 200
90
+ * and happily reported the earlier fix working. That is why `freshness` exists;
91
+ * `ctimeNs` differed in 100 of 100 of those same cycles.
92
+ *
93
+ * `bigint: true` is used because `ctimeNs` exists only in that variant.
94
+ *
95
+ * Test seam: `setStatForTests` replaces this reader so a test can vary `dev`,
96
+ * `ino`, and `ctimeNs` independently. Mirroring the implementation's string
97
+ * format in test setup proves nothing about which components production uses —
98
+ * an audit removed `dev` and all forty tests still passed.
99
+ *
100
+ * UNVERIFIED: the plain `ino` is reported to be 0 on NTFS while the bigint form
101
+ * carries the file index, and the zero-ino guard exists for that case. Neither
102
+ * Darwin nor Linux CI can confirm it and no pinned-Bun Windows probe has run.
103
+ * It is defensive code, not a demonstrated platform fact.
104
+ */
105
+ type StatReader = (path: string) => { dev: bigint; ino: bigint; ctimeNs: bigint };
106
+
107
+ const defaultStatReader: StatReader = path => {
108
+ const s = statSync(path, { bigint: true });
109
+ return { dev: s.dev, ino: s.ino, ctimeNs: s.ctimeNs };
110
+ };
111
+
112
+ let statReader: StatReader = defaultStatReader;
113
+
114
+ /** Test seam: drive dev / ino / ctime independently. */
115
+ export function setStatForTests(reader: StatReader | null): void {
116
+ statReader = reader ?? defaultStatReader;
117
+ }
118
+
119
+ function observe(targetPath: string): PathObservation | null {
120
+ try {
121
+ const s = statReader(targetPath);
122
+ if (s.ino === 0n) return null;
123
+ return { object: `${s.dev}:${s.ino}`, freshness: `${s.ctimeNs}` };
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ function memoValue(seen: PathObservation): HardenedIdentity {
130
+ return `${seen.object}:${seen.freshness}`;
131
+ }
34
132
 
35
- const hardenedDirectories = new Set<string>();
36
- const hardenedPaths = new Set<string>();
37
- /** Paths whose harden TIMED OUT this process: do not re-stall every loadConfig on them. */
38
- const timedOutPaths = new Set<string>();
133
+ /**
134
+ * True only when this exact FILE was hardened, not merely this pathname.
135
+ *
136
+ * The memo used to be a `Set<string>` of paths. A stable destination — such as
137
+ * the coordinator database `hardenStableLockFile` hardens — can be unlinked and
138
+ * recreated at the same name, and the replacement inherited the previous file's
139
+ * hardening while never having been through icacls. Ephemeral temps escaped this
140
+ * only because atomic writers call `forgetEphemeralSecretPath` once the temp is
141
+ * gone; nothing does that for a stable path.
142
+ */
143
+ function memoSatisfied(cache: Map<string, HardenedIdentity>, targetPath: string): boolean {
144
+ const remembered = cache.get(targetPath);
145
+ if (remembered === undefined) return false;
146
+ const current = observe(targetPath);
147
+ // Unreadable now is not "unchanged": re-harden rather than trust a value we
148
+ // cannot confirm still describes what is there.
149
+ //
150
+ // A miss RETIRES the entry rather than leaving it. Keeping it left the cache in
151
+ // a state nothing could justify: after a mismatch and a failed re-harden, the
152
+ // stale value survived, so restoring the old identity would satisfy it again
153
+ // without any ACL work. That needs exact-identity ABA to bite — outside the
154
+ // proof bound this unit claims — but "the consequence is out of scope" is not a
155
+ // reason to keep an entry we have just proven does not describe what is there.
156
+ if (current === null || memoValue(current) !== remembered) {
157
+ cache.delete(targetPath);
158
+ return false;
159
+ }
160
+ return true;
161
+ }
162
+
163
+ /**
164
+ * Record a harden ONLY if the file we hardened is still the file at that path.
165
+ *
166
+ * Reading identity after the ACL sequence returns answers "what is there now",
167
+ * which is not the same question as "what did icacls operate on". A replacement
168
+ * landing mid-sequence — probed by swapping the file during the final
169
+ * `/remove:g` — made the memo remember the REPLACEMENT as hardened, so the next
170
+ * acquisition skipped ACL work on a file that had never seen it:
171
+ *
172
+ * {identityChangedDuringHarden: true, callsForOriginal: 3, totalCalls: 3,
173
+ * replacementWasHardened: false}
174
+ *
175
+ * So the OBJECT is captured before the sequence and compared after it. Only the
176
+ * object — `dev:ino` — because icacls changes permissions, and `ctimeNs` moves
177
+ * when permissions change (probed: `{ctimeChangedByChmod: true}`). Comparing the
178
+ * full identity across the call would have rejected every successful harden and
179
+ * failed closed on the first harden on Windows: the check would have been
180
+ * demanding that an operation not do the thing it exists to do.
181
+ *
182
+ * The memo then stores the object plus the freshness read AFTER hardening, which
183
+ * is the state a later lookup should match.
184
+ *
185
+ * A changed object, or an unreadable observation at either end, means we cannot
186
+ * say what was hardened: the memo is cleared rather than written, and required
187
+ * callers fail closed. An optional caller soft-fails, as it does for every other
188
+ * unproven ACL.
189
+ *
190
+ * Returns true when the harden may be reported successful.
191
+ */
192
+ function recordHarden(
193
+ cache: Map<string, HardenedIdentity>,
194
+ targetPath: string,
195
+ before: PathObservation | null,
196
+ ): boolean {
197
+ const after = observe(targetPath);
198
+ if (before === null || after === null || before.object !== after.object) {
199
+ // Never leave a memo behind for a file we cannot vouch for, including one
200
+ // written by an earlier successful harden of a now-replaced file.
201
+ cache.delete(targetPath);
202
+ return false;
203
+ }
204
+ cache.set(targetPath, memoValue(after));
205
+ return true;
206
+ }
207
+
208
+ const SUBSTITUTED_DIAGNOSTIC =
209
+ "ACL hardening could not be attributed — the file at this path changed during hardening";
39
210
 
40
211
  export interface HardenResult {
41
212
  ok: boolean;
@@ -51,6 +222,12 @@ export interface HardenOptions {
51
222
  * Must NOT be a parent directory — directory ACLs are not authoritative for new files.
52
223
  */
53
224
  timeoutMemoKey?: string;
225
+ /**
226
+ * Consume the one recovery attempt for a previously timed-out memo key.
227
+ * Only a caller that owns its own single-flight and bounded retry policy should
228
+ * set this. It never clears or bypasses an already-consumed timeout memo.
229
+ */
230
+ retryTimedOutOnce?: boolean;
54
231
  }
55
232
 
56
233
  /**
@@ -58,9 +235,23 @@ export interface HardenOptions {
58
235
  * timeout retry and the diagnostic verification pass (no per-attempt fresh budget:
59
236
  * loadConfig hardens dir+config+auth sequentially, so per-attempt budgets stack
60
237
  * into multi-minute startup stalls). Override with OPENCODEX_ACL_TIMEOUT_MS
61
- * (integer ms, clamped to [1000, 60000]; invalid values fall back to 5000).
238
+ * (integer ms, clamped to [1000, 60000]; invalid values fall back to 30000).
239
+ *
240
+ * The default was 5s until #1156. One envelope has to cover the whole sequence —
241
+ * `/grant:r`, `/inheritance:r`, `/remove:g`, plus the conditional `/findsid`
242
+ * verification — and on machines where icacls is slow (Defender real-time scanning,
243
+ * roaming profiles, a domain-controller round trip) 5s ran out mid-sequence. The
244
+ * harden then failed closed, the native-main owner published a permanent
245
+ * `unavailable`, and every native request returned 503 until restart. A slow start
246
+ * is recoverable; that is not.
247
+ *
248
+ * The cost is honest and worth stating: because loadConfig hardens three paths
249
+ * sequentially, the timeout-path worst case at load is ~90s, and the owner path
250
+ * (initial call + one recovery) is ~60.25s. Both require icacls to be
251
+ * pathologically slow on every call; a healthy machine finishes in milliseconds
252
+ * and sees no change. Operators who prefer the old bound can set the env override.
62
253
  */
63
- const HARDEN_DEADLINE_DEFAULT_MS = 5_000;
254
+ const HARDEN_DEADLINE_DEFAULT_MS = 30_000;
64
255
  const HARDEN_DEADLINE_MIN_MS = 1_000;
65
256
  const HARDEN_DEADLINE_MAX_MS = 60_000;
66
257
 
@@ -83,22 +274,55 @@ export interface IcaclsResult {
83
274
  type IcaclsRunner = (args: string[], timeoutMs: number) => IcaclsResult;
84
275
  type AsyncIcaclsRunner = (args: string[], timeoutMs: number) => Promise<IcaclsResult>;
85
276
 
277
+ function resolveIcaclsExecutable(): string {
278
+ // Same authority as schtasks/powershell: never take icacls from PATH.
279
+ // A bun-shim or stripped PATH makes `icacls.exe` throw ENOENT, which used to
280
+ // surface as "filesystem may not support per-user NTFS ACLs".
281
+ return resolveTrustedWindowsIcaclsExe();
282
+ }
283
+
284
+ function spawnFailedResult(): IcaclsResult {
285
+ return { success: false, exitCode: null, timedOut: false, stdout: "" };
286
+ }
287
+
288
+ /**
289
+ * Spawn icacls asynchronously, or return null when the executable cannot be
290
+ * launched. The pipe/ignore stdio literals stay inferred here so `stdout` keeps
291
+ * its `ReadableStream` type instead of widening to the generic default.
292
+ */
293
+ function trySpawnIcacls(args: string[]) {
294
+ try {
295
+ return Bun.spawn([resolveIcaclsExecutable(), ...args], {
296
+ stdin: "ignore",
297
+ stdout: "pipe",
298
+ stderr: "ignore",
299
+ windowsHide: true,
300
+ });
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+
86
306
  function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
87
307
  // Bun.spawnSync with windowsHide: Node execFileSync has hung under the GUI/proxy even
88
308
  // with windowsHide, and console-subsystem tools flash a visible window otherwise.
89
- const result = Bun.spawnSync(["icacls.exe", ...args], {
90
- stdin: "ignore",
91
- stdout: "pipe",
92
- stderr: "ignore",
93
- timeout: timeoutMs,
94
- windowsHide: true,
95
- });
96
- return {
97
- success: result.success,
98
- exitCode: result.exitCode,
99
- timedOut: result.exitedDueToTimeout ?? false,
100
- stdout: result.stdout ? result.stdout.toString() : "",
101
- };
309
+ try {
310
+ const result = Bun.spawnSync([resolveIcaclsExecutable(), ...args], {
311
+ stdin: "ignore",
312
+ stdout: "pipe",
313
+ stderr: "ignore",
314
+ timeout: timeoutMs,
315
+ windowsHide: true,
316
+ });
317
+ return {
318
+ success: result.success,
319
+ exitCode: result.exitCode,
320
+ timedOut: result.exitedDueToTimeout ?? false,
321
+ stdout: result.stdout ? result.stdout.toString() : "",
322
+ };
323
+ } catch {
324
+ return spawnFailedResult();
325
+ }
102
326
  }
103
327
 
104
328
  /**
@@ -107,12 +331,8 @@ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
107
331
  * we still await process exit before classifying so settlement is confirmed.
108
332
  */
109
333
  async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
110
- const proc = Bun.spawn(["icacls.exe", ...args], {
111
- stdin: "ignore",
112
- stdout: "pipe",
113
- stderr: "ignore",
114
- windowsHide: true,
115
- });
334
+ const proc = trySpawnIcacls(args);
335
+ if (!proc) return spawnFailedResult();
116
336
  let timedOutByUs = false;
117
337
  const timer = setTimeout(() => {
118
338
  timedOutByUs = true;
@@ -151,9 +371,21 @@ export function setAsyncIcaclsRunnerForTests(runner: AsyncIcaclsRunner | null):
151
371
  asyncIcaclsRunner = runner ?? defaultAsyncIcaclsRunner;
152
372
  }
153
373
 
154
- /** Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner. */
374
+ /**
375
+ * Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner.
376
+ *
377
+ * Faking win32 on a host without System32 also has to supply a principal, or
378
+ * every forced-branch test would fail on the identity lookup instead of
379
+ * exercising icacls. The synthetic value is registered with the resolver, not
380
+ * chosen here, so a test that injects its own runner still wins.
381
+ */
382
+ const SYNTHETIC_TEST_PRINCIPAL = "*S-1-5-21-1-2-3-1001";
383
+
155
384
  export function setPlatformForTests(value: string | null): void {
156
385
  platformOverride = value;
386
+ setSyntheticWindowsPrincipalForTests(
387
+ value === "win32" && platform !== "win32" ? SYNTHETIC_TEST_PRINCIPAL : null,
388
+ );
157
389
  }
158
390
 
159
391
  /** Test seam: injectable clock for deadline tests (no real sleeps). */
@@ -173,11 +405,44 @@ export function forgetHardenedSecretPath(targetPath: string): void {
173
405
  hardenedPaths.delete(targetPath);
174
406
  }
175
407
 
408
+ /**
409
+ * Ephemeral-path lifecycle release: clears the success memo AND any timeout
410
+ * memo keyed by THIS TEMP path in both namespaces. Call only after the temp is
411
+ * proven absent (successful rename, successful unlink, ENOENT, or an explicit
412
+ * non-existence check). Never pass a stable destination: destination-keyed
413
+ * timeout memos are intentional anti-restall state and are not touched here.
414
+ */
415
+ export function forgetEphemeralSecretPath(tempPath: string): void {
416
+ hardenedPaths.delete(tempPath);
417
+ timedOutPaths.delete(`required:${tempPath}`);
418
+ timedOutPaths.delete(`optional:${tempPath}`);
419
+ }
420
+
421
+ /** Directory counterpart for a proven-absent ephemeral staging root. */
422
+ export function forgetEphemeralSecretDir(tempPath: string): void {
423
+ hardenedDirectories.delete(tempPath);
424
+ timedOutPaths.delete(`required:${tempPath}`);
425
+ timedOutPaths.delete(`optional:${tempPath}`);
426
+ }
427
+
428
+ /** Test seam: timeout memo sets return to baseline after ephemeral cleanup. */
429
+ export function timedOutSecretPathCountForTests(): number {
430
+ return timedOutPaths.size;
431
+ }
432
+
176
433
  /** Test seam for proving ephemeral success memos do not grow across replacements. */
177
434
  export function hardenedSecretPathCountForTests(): number {
178
435
  return hardenedPaths.size;
179
436
  }
180
437
 
438
+ /**
439
+ * Directory counterpart. It had no seam, and that absence hid a real gap: a
440
+ * directory-only pathname memo passed every file-based test in this suite.
441
+ */
442
+ export function hardenedSecretDirCountForTests(): number {
443
+ return hardenedDirectories.size;
444
+ }
445
+
181
446
  function effectivePlatform(): string {
182
447
  return platformOverride ?? platform;
183
448
  }
@@ -197,17 +462,26 @@ function icaclsError(step: string, result: IcaclsResult): NodeJS.ErrnoException
197
462
  }
198
463
 
199
464
  /**
200
- * Return the current Windows username from the environment.
201
- * Falls back to USERDOMAIN\USERNAME if USERNAME alone is ambiguous.
202
- * The value is used directly in icacls arguments, so it must be present.
465
+ * The ACL principal is the effective token SID and nothing else.
466
+ *
467
+ * There is no name-shaped fallback here, and that absence is the fix for #1149
468
+ * rather than an omission. `USERDOMAIN\USERNAME` has the right shape but is not
469
+ * evidence of the current token's subject, and both variables are writable by
470
+ * the process that launched us. Granting Full Control to a wrong principal and
471
+ * then running `/inheritance:r` is destructive in both directions: another
472
+ * account can be left holding the secret, or the file can be left with no ACE
473
+ * the current user can use. When the SID cannot be resolved we decline.
474
+ *
475
+ * Non-Windows hosts that force this branch through `setPlatformForTests` get
476
+ * their principal from `setSyntheticWindowsPrincipalForTests`, which lives with
477
+ * the resolver so an injected runner can still take precedence over it.
203
478
  */
204
- function currentWindowsUser(): string | undefined {
205
- const username = env["USERNAME"];
206
- const domain = env["USERDOMAIN"];
207
- if (!username) return undefined;
208
- // USERDOMAIN is the machine/domain name; USERNAME is the account name.
209
- // icacls accepts "DOMAIN\User" or just "User" for local accounts.
210
- return domain ? `${domain}\\${username}` : username;
479
+ function currentWindowsPrincipal(deadline: number): string {
480
+ return resolveCurrentWindowsPrincipal(deadline - nowFn());
481
+ }
482
+
483
+ async function currentWindowsPrincipalAsync(deadline: number): Promise<string> {
484
+ return resolveCurrentWindowsPrincipalAsync(deadline - nowFn());
211
485
  }
212
486
 
213
487
  /**
@@ -227,10 +501,7 @@ function grantAce(user: string, directory: boolean): string {
227
501
  }
228
502
 
229
503
  function runIcacls(targetPath: string, directory: boolean, deadline: number): void {
230
- const user = currentWindowsUser();
231
- if (!user) {
232
- throw new Error("Cannot determine current Windows user for ACL hardening");
233
- }
504
+ const principal = currentWindowsPrincipal(deadline);
234
505
 
235
506
  // The deadline is owned by hardenEntry (total budget incl. retry + verification).
236
507
  const run = (step: string, args: string[]): IcaclsResult => {
@@ -247,7 +518,7 @@ function runIcacls(targetPath: string, directory: boolean, deadline: number): vo
247
518
 
248
519
  // Step 1: grant current user full control BEFORE any destructive ACL change.
249
520
  // If this fails, inheritance is untouched and the writer keeps inherited access.
250
- runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(user, directory)]);
521
+ runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(principal, directory)]);
251
522
 
252
523
  // Step 2: disable inheritance and remove inherited ACEs. The explicit owner ACE
253
524
  // from step 1 survives this transition, so a later failure still leaves cleanup access.
@@ -276,10 +547,7 @@ function runIcacls(targetPath: string, directory: boolean, deadline: number): vo
276
547
 
277
548
  /** Async counterpart of runIcacls — same step order and timeout/error classification (#612). */
278
549
  async function runIcaclsAsync(targetPath: string, directory: boolean, deadline: number): Promise<void> {
279
- const user = currentWindowsUser();
280
- if (!user) {
281
- throw new Error("Cannot determine current Windows user for ACL hardening");
282
- }
550
+ const principal = await currentWindowsPrincipalAsync(deadline);
283
551
 
284
552
  const run = async (step: string, args: string[]): Promise<IcaclsResult> => {
285
553
  const remaining = deadline - nowFn();
@@ -293,7 +561,7 @@ async function runIcaclsAsync(targetPath: string, directory: boolean, deadline:
293
561
  if (!result.success) throw icaclsError(step, result);
294
562
  };
295
563
 
296
- await runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(user, directory)]);
564
+ await runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(principal, directory)]);
297
565
  await runOrThrow("/inheritance:r", [targetPath, "/inheritance:r"]);
298
566
 
299
567
  const removal = await run("/remove:g", [targetPath, "/remove:g", ...BROAD_SIDS]);
@@ -327,6 +595,8 @@ function sanitizeDiagnostics(error: unknown): string {
327
595
  return `ACL hardening failed (${code}) — permission denied running icacls`;
328
596
  case "EICACLS":
329
597
  return "ACL hardening failed (EICACLS) — icacls command error; filesystem may not support per-user NTFS ACLs";
598
+ case "EACLIDENTITY":
599
+ return "ACL hardening failed (EACLIDENTITY) — the effective Windows account SID could not be resolved";
330
600
  default:
331
601
  return `ACL hardening failed${code ? ` (${code})` : ""} — filesystem may not support per-user NTFS ACLs`;
332
602
  }
@@ -337,6 +607,60 @@ function isTimeoutError(error: unknown): boolean {
337
607
  && String((error as NodeJS.ErrnoException).code) === "ETIMEDOUT";
338
608
  }
339
609
 
610
+ /** Preserve only the bounded machine-readable cause on a sanitized public error. */
611
+ function sanitizedAclError(diagnostics: string, cause: unknown): NodeJS.ErrnoException {
612
+ const error = new Error(diagnostics) as NodeJS.ErrnoException;
613
+ const code = cause && typeof cause === "object" && "code" in cause
614
+ ? String((cause as { code?: unknown }).code)
615
+ : "";
616
+ // EACLIDENTITY belongs here for the same reason as the rest: a caller that
617
+ // catches a required-mode failure has to tell "the SID could not be resolved"
618
+ // apart from "icacls stalled". Without it the code was dropped and only the
619
+ // message carried the cause, which no caller can branch on.
620
+ if (
621
+ code === "ETIMEDOUT" ||
622
+ code === "EICACLS" ||
623
+ code === "EACCES" ||
624
+ code === "EPERM" ||
625
+ code === "EACLIDENTITY"
626
+ ) {
627
+ error.code = code;
628
+ }
629
+ return error;
630
+ }
631
+
632
+ function previousTimeoutError(retryConsumed: boolean): NodeJS.ErrnoException {
633
+ if (retryConsumed) {
634
+ const error = new Error(
635
+ "ACL hardening skipped — the previous timeout recovery was already consumed",
636
+ ) as NodeJS.ErrnoException;
637
+ error.code = "EACLRETRYEXHAUSTED";
638
+ return error;
639
+ }
640
+ return sanitizedAclError(
641
+ "ACL hardening skipped — previous attempt timed out",
642
+ Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }),
643
+ );
644
+ }
645
+
646
+ /** Consume, but never reset, the single explicit recovery attempt for this key. */
647
+ function timeoutMemoErrorIfBlocked(
648
+ memoKey: string,
649
+ opts: HardenOptions,
650
+ ): NodeJS.ErrnoException | null {
651
+ const retryConsumed = timedOutPaths.get(memoKey);
652
+ if (retryConsumed === undefined) return null;
653
+ if (opts.retryTimedOutOnce && retryConsumed === false) {
654
+ timedOutPaths.set(memoKey, true);
655
+ return null;
656
+ }
657
+ return previousTimeoutError(retryConsumed);
658
+ }
659
+
660
+ function recordTimeout(memoKey: string): void {
661
+ if (!timedOutPaths.has(memoKey)) timedOutPaths.set(memoKey, false);
662
+ }
663
+
340
664
  /**
341
665
  * Diagnostic-only post-timeout probe (never promotes to ok:true — a clean /findsid
342
666
  * does not prove inheritance was disabled or the user grant ran; only a fully
@@ -393,16 +717,18 @@ function hardenEntry(
393
717
  targetPath: string,
394
718
  directory: boolean,
395
719
  opts: HardenOptions,
396
- cache: Set<string>,
720
+ cache: Map<string, HardenedIdentity>,
397
721
  ): HardenResult {
398
- if (!existsSync(targetPath)) return { ok: true };
722
+ // Observed absence retires the memo. Leaving it would let a later file at this
723
+ // path satisfy the cache if the filesystem ever hands back a matching identity.
724
+ if (!existsSync(targetPath)) { cache.delete(targetPath); return { ok: true }; }
399
725
  if (effectivePlatform() !== "win32") return { ok: true };
400
- if (cache.has(targetPath)) return { ok: true };
726
+ if (memoSatisfied(cache, targetPath)) return { ok: true };
401
727
  const memoKey = timeoutMemoKey(targetPath, opts);
402
- if (timedOutPaths.has(memoKey)) {
403
- const diagnostics = "ACL hardening skipped — previous attempt timed out";
404
- if (opts.required) throw new Error(diagnostics);
405
- return { ok: false, diagnostics };
728
+ const timeoutMemoError = timeoutMemoErrorIfBlocked(memoKey, opts);
729
+ if (timeoutMemoError) {
730
+ if (opts.required) throw timeoutMemoError;
731
+ return { ok: false, diagnostics: timeoutMemoError.message };
406
732
  }
407
733
 
408
734
  const deadline = nowFn() + resolveHardenDeadlineMs();
@@ -410,10 +736,18 @@ function hardenEntry(
410
736
  for (let attempt = 0; attempt < 2; attempt++) {
411
737
  if (attempt > 0 && deadline - nowFn() <= 0) break; // retry only while budget remains
412
738
  try {
739
+ // Captured BEFORE the sequence: this is the file we are about to harden.
740
+ const before = observe(targetPath);
413
741
  runIcacls(targetPath, directory, deadline);
414
- cache.add(targetPath);
742
+ if (!recordHarden(cache, targetPath, before)) {
743
+ if (opts.required) throw new Error(SUBSTITUTED_DIAGNOSTIC);
744
+ return { ok: false, diagnostics: SUBSTITUTED_DIAGNOSTIC };
745
+ }
746
+ timedOutPaths.delete(memoKey);
415
747
  return { ok: true };
416
748
  } catch (err) {
749
+ // A substitution is not a transient icacls stall; do not spend the retry on it.
750
+ if (err instanceof Error && err.message === SUBSTITUTED_DIAGNOSTIC) throw err;
417
751
  lastErr = err;
418
752
  if (!isTimeoutError(err)) break; // real failures do not retry
419
753
  }
@@ -421,14 +755,14 @@ function hardenEntry(
421
755
 
422
756
  const diagnostics = sanitizeDiagnostics(lastErr);
423
757
  if (isTimeoutError(lastErr)) {
424
- timedOutPaths.add(memoKey);
758
+ recordTimeout(memoKey);
425
759
  const state = describeAclStateAfterTimeout(targetPath, deadline);
426
760
  const annotated = `${diagnostics}; ${state}`;
427
- if (opts.required) throw new Error(annotated);
761
+ if (opts.required) throw sanitizedAclError(annotated, lastErr);
428
762
  console.warn(`[opencodex] ${annotated} — continuing without NTFS ACL harden`);
429
763
  return { ok: false, diagnostics: annotated };
430
764
  }
431
- if (opts.required) throw new Error(diagnostics);
765
+ if (opts.required) throw sanitizedAclError(diagnostics, lastErr);
432
766
  return { ok: false, diagnostics };
433
767
  }
434
768
 
@@ -437,16 +771,16 @@ async function hardenEntryAsync(
437
771
  targetPath: string,
438
772
  directory: boolean,
439
773
  opts: HardenOptions,
440
- cache: Set<string>,
774
+ cache: Map<string, HardenedIdentity>,
441
775
  ): Promise<HardenResult> {
442
- if (!existsSync(targetPath)) return { ok: true };
776
+ if (!existsSync(targetPath)) { cache.delete(targetPath); return { ok: true }; }
443
777
  if (effectivePlatform() !== "win32") return { ok: true };
444
- if (cache.has(targetPath)) return { ok: true };
778
+ if (memoSatisfied(cache, targetPath)) return { ok: true };
445
779
  const memoKey = timeoutMemoKey(targetPath, opts);
446
- if (timedOutPaths.has(memoKey)) {
447
- const diagnostics = "ACL hardening skipped — previous attempt timed out";
448
- if (opts.required) throw new Error(diagnostics);
449
- return { ok: false, diagnostics };
780
+ const timeoutMemoError = timeoutMemoErrorIfBlocked(memoKey, opts);
781
+ if (timeoutMemoError) {
782
+ if (opts.required) throw timeoutMemoError;
783
+ return { ok: false, diagnostics: timeoutMemoError.message };
450
784
  }
451
785
 
452
786
  const deadline = nowFn() + resolveHardenDeadlineMs();
@@ -454,10 +788,16 @@ async function hardenEntryAsync(
454
788
  for (let attempt = 0; attempt < 2; attempt++) {
455
789
  if (attempt > 0 && deadline - nowFn() <= 0) break;
456
790
  try {
791
+ const before = observe(targetPath);
457
792
  await runIcaclsAsync(targetPath, directory, deadline);
458
- cache.add(targetPath);
793
+ if (!recordHarden(cache, targetPath, before)) {
794
+ if (opts.required) throw new Error(SUBSTITUTED_DIAGNOSTIC);
795
+ return { ok: false, diagnostics: SUBSTITUTED_DIAGNOSTIC };
796
+ }
797
+ timedOutPaths.delete(memoKey);
459
798
  return { ok: true };
460
799
  } catch (err) {
800
+ if (err instanceof Error && err.message === SUBSTITUTED_DIAGNOSTIC) throw err;
461
801
  lastErr = err;
462
802
  if (!isTimeoutError(err)) break;
463
803
  }
@@ -465,14 +805,14 @@ async function hardenEntryAsync(
465
805
 
466
806
  const diagnostics = sanitizeDiagnostics(lastErr);
467
807
  if (isTimeoutError(lastErr)) {
468
- timedOutPaths.add(memoKey);
808
+ recordTimeout(memoKey);
469
809
  const state = await describeAclStateAfterTimeoutAsync(targetPath, deadline);
470
810
  const annotated = `${diagnostics}; ${state}`;
471
- if (opts.required) throw new Error(annotated);
811
+ if (opts.required) throw sanitizedAclError(annotated, lastErr);
472
812
  console.warn(`[opencodex] ${annotated} — continuing without NTFS ACL harden`);
473
813
  return { ok: false, diagnostics: annotated };
474
814
  }
475
- if (opts.required) throw new Error(diagnostics);
815
+ if (opts.required) throw sanitizedAclError(diagnostics, lastErr);
476
816
  return { ok: false, diagnostics };
477
817
  }
478
818