patchwarden 1.5.1 → 1.6.1

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 (573) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +152 -25
  3. package/README.md +120 -23
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentStore.js +42 -34
  6. package/dist/config.d.ts +15 -0
  7. package/dist/config.js +66 -13
  8. package/dist/control/fileManager.d.ts +2 -0
  9. package/dist/control/fileManager.js +39 -0
  10. package/dist/control/middleware/auth.d.ts +6 -0
  11. package/dist/control/middleware/auth.js +14 -5
  12. package/dist/control/middleware/static.js +16 -3
  13. package/dist/control/routeTable.d.ts +24 -0
  14. package/dist/control/routeTable.js +263 -0
  15. package/dist/control/routes/audit.js +17 -7
  16. package/dist/control/routes/evidence.js +1 -1
  17. package/dist/control/routes/lineage.js +1 -1
  18. package/dist/control/routes/process.d.ts +7 -0
  19. package/dist/control/routes/process.js +329 -42
  20. package/dist/control/routes/sessions.d.ts +9 -1
  21. package/dist/control/routes/sessions.js +84 -21
  22. package/dist/control/routes/status.d.ts +46 -1
  23. package/dist/control/routes/status.js +72 -15
  24. package/dist/control/routes/taskActions.js +24 -60
  25. package/dist/control/routes/tasks.d.ts +4 -0
  26. package/dist/control/routes/tasks.js +61 -62
  27. package/dist/control/routes/workspace.js +50 -23
  28. package/dist/control/runtime.d.ts +3 -2
  29. package/dist/control/runtime.js +63 -30
  30. package/dist/control/server.js +36 -365
  31. package/dist/control/shared.d.ts +25 -1
  32. package/dist/control/shared.js +64 -11
  33. package/dist/direct/directAudit.js +234 -225
  34. package/dist/direct/directGuards.d.ts +2 -1
  35. package/dist/direct/directGuards.js +42 -29
  36. package/dist/direct/directPatch.d.ts +6 -1
  37. package/dist/direct/directPatch.js +55 -8
  38. package/dist/direct/directSessionStore.d.ts +16 -6
  39. package/dist/direct/directSessionStore.js +127 -35
  40. package/dist/direct/directVerification.js +8 -16
  41. package/dist/doctor.js +62 -52
  42. package/dist/goal/acceptanceEngine.d.ts +1 -1
  43. package/dist/goal/goalProgress.js +75 -69
  44. package/dist/goal/goalReport.js +5 -12
  45. package/dist/goal/goalStatus.d.ts +2 -0
  46. package/dist/goal/goalStatus.js +10 -3
  47. package/dist/goal/goalStore.d.ts +9 -2
  48. package/dist/goal/goalStore.js +152 -45
  49. package/dist/goal/handoffExport.js +6 -6
  50. package/dist/goal/specKitImport.js +97 -96
  51. package/dist/goal/subgoalSync.js +11 -9
  52. package/dist/goal/worktreeManager.d.ts +2 -0
  53. package/dist/goal/worktreeManager.js +209 -136
  54. package/dist/httpServer.js +111 -32
  55. package/dist/logging.js +5 -3
  56. package/dist/policy/projectPolicy.js +5 -2
  57. package/dist/release/releaseGate.d.ts +1 -1
  58. package/dist/release/releaseGate.js +78 -25
  59. package/dist/runner/agentInvocation.d.ts +14 -0
  60. package/dist/runner/agentInvocation.js +96 -4
  61. package/dist/runner/changeCapture.d.ts +13 -5
  62. package/dist/runner/changeCapture.js +243 -61
  63. package/dist/runner/postTaskCleanup.js +48 -19
  64. package/dist/runner/processSecurity.d.ts +56 -0
  65. package/dist/runner/processSecurity.js +373 -0
  66. package/dist/runner/runTask.d.ts +1 -1
  67. package/dist/runner/runTask.js +115 -87
  68. package/dist/runner/simpleProcess.d.ts +3 -0
  69. package/dist/runner/simpleProcess.js +50 -36
  70. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  71. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  72. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  73. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  74. package/dist/runner/taskStatusStore.d.ts +11 -0
  75. package/dist/runner/taskStatusStore.js +24 -0
  76. package/dist/runner/watch.d.ts +14 -1
  77. package/dist/runner/watch.js +256 -48
  78. package/dist/security/contentRedaction.js +2 -2
  79. package/dist/security/discoveryTokenStore.d.ts +3 -2
  80. package/dist/security/discoveryTokenStore.js +17 -2
  81. package/dist/security/loopbackHost.d.ts +2 -0
  82. package/dist/security/loopbackHost.js +16 -0
  83. package/dist/security/pathGuard.js +32 -17
  84. package/dist/security/planGuard.js +2 -1
  85. package/dist/security/runtimeGuard.js +9 -6
  86. package/dist/security/secretComparison.d.ts +3 -0
  87. package/dist/security/secretComparison.js +11 -0
  88. package/dist/security/sensitiveGuard.d.ts +5 -0
  89. package/dist/security/sensitiveGuard.js +27 -8
  90. package/dist/security/toolInvocationGuard.d.ts +2 -2
  91. package/dist/security/toolInvocationGuard.js +1 -1
  92. package/dist/security/workspaceRootGuard.d.ts +8 -0
  93. package/dist/security/workspaceRootGuard.js +47 -0
  94. package/dist/smoke-test.js +24 -23
  95. package/dist/tools/catalog/index.d.ts +4 -0
  96. package/dist/tools/catalog/index.js +4 -0
  97. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  98. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  99. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  100. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  101. package/dist/tools/definitions/toolDefs.js +1177 -0
  102. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  103. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  104. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
  105. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
  106. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  107. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
  108. package/dist/tools/diagnostics/index.d.ts +6 -0
  109. package/dist/tools/diagnostics/index.js +6 -0
  110. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  111. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  112. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  113. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  114. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  115. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  116. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  117. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  118. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  119. package/dist/tools/direct/index.d.ts +3 -0
  120. package/dist/tools/direct/index.js +3 -0
  121. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  122. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  123. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  124. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  125. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  126. package/dist/tools/discovery/index.d.ts +3 -0
  127. package/dist/tools/discovery/index.js +3 -0
  128. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  129. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  130. package/dist/tools/dispatch/coreDispatch.js +32 -31
  131. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  132. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  133. package/dist/tools/dispatch/directDispatch.js +11 -10
  134. package/dist/tools/dispatch/goalDispatch.js +5 -4
  135. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  136. package/dist/tools/dispatch/validation.d.ts +7 -0
  137. package/dist/tools/dispatch/validation.js +71 -0
  138. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  139. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  140. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  141. package/dist/tools/goals/index.d.ts +3 -0
  142. package/dist/tools/goals/index.js +3 -0
  143. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  144. package/dist/tools/registry.d.ts +7 -11
  145. package/dist/tools/registry.js +17 -1178
  146. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  147. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  148. package/dist/tools/release/index.d.ts +2 -0
  149. package/dist/tools/release/index.js +2 -0
  150. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  151. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  152. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  153. package/dist/tools/tasks/cancelTask.js +85 -0
  154. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
  155. package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
  156. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  157. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  158. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  159. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  160. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  161. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  162. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  163. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  164. package/dist/tools/tasks/index.d.ts +18 -0
  165. package/dist/tools/tasks/index.js +18 -0
  166. package/dist/tools/tasks/killTask.d.ts +1 -0
  167. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  168. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  169. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  170. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  171. package/dist/tools/tasks/retryTask.js +60 -0
  172. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  173. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
  174. package/dist/tools/tasks/runVerification.js +38 -0
  175. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  176. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  177. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  178. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  179. package/dist/tools/workspace/applyPatch.js +53 -0
  180. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  181. package/dist/tools/workspace/index.d.ts +10 -0
  182. package/dist/tools/workspace/index.js +10 -0
  183. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  184. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  185. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  186. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  187. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  188. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  189. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  190. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  191. package/dist/tools/workspace/syncFile.js +134 -0
  192. package/dist/utils/atomicFile.d.ts +7 -0
  193. package/dist/utils/atomicFile.js +48 -0
  194. package/dist/utils/boundedFile.d.ts +8 -0
  195. package/dist/utils/boundedFile.js +93 -0
  196. package/dist/utils/lockedJsonFile.d.ts +15 -0
  197. package/dist/utils/lockedJsonFile.js +224 -0
  198. package/dist/utils/platform.d.ts +11 -0
  199. package/dist/utils/platform.js +26 -0
  200. package/dist/utils/stableJson.d.ts +2 -0
  201. package/dist/utils/stableJson.js +12 -0
  202. package/dist/version.d.ts +3 -2
  203. package/dist/version.js +3 -2
  204. package/dist/watcherStatus.d.ts +2 -2
  205. package/dist/watcherStatus.js +18 -0
  206. package/docs/CODE_WIKI.md +808 -143
  207. package/docs/architecture.md +15 -55
  208. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  209. package/docs/control-center/README.md +4 -4
  210. package/docs/dashboard-overview.md +4 -4
  211. package/docs/desktop-app.md +175 -0
  212. package/docs/evidence-pack-schema.md +3 -3
  213. package/docs/open-source-application.md +17 -11
  214. package/docs/release-checklist.md +1 -1
  215. package/docs/release-evidence.md +39 -29
  216. package/docs/task-safe-review-workflow.md +5 -5
  217. package/docs/threat-model.md +19 -1
  218. package/examples/config.example.json +4 -2
  219. package/package.json +27 -10
  220. package/scripts/README.md +15 -6
  221. package/scripts/build.js +11 -0
  222. package/scripts/checks/brand-check.js +3 -0
  223. package/scripts/checks/build-output-check.js +37 -0
  224. package/scripts/checks/control-center-smoke.js +91 -6
  225. package/scripts/checks/control-smoke.js +19 -8
  226. package/scripts/checks/http-mcp-smoke.js +82 -4
  227. package/scripts/checks/lifecycle-smoke.js +10 -10
  228. package/scripts/checks/mcp-manifest-check.js +1 -1
  229. package/scripts/checks/mcp-smoke.js +27 -3
  230. package/scripts/checks/package-manifest-check.js +20 -7
  231. package/scripts/checks/unit-tests.js +3 -5
  232. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  233. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  234. package/scripts/control/manage-patchwarden.ps1 +69 -4
  235. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  236. package/scripts/control/run-background-supervisor.ps1 +41 -0
  237. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  238. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  239. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  240. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  241. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  242. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  243. package/scripts/lib/clean-generated-output.js +11 -0
  244. package/scripts/lib/file-discovery.js +12 -0
  245. package/scripts/release/desktop-preflight.js +211 -0
  246. package/scripts/release/pack-clean.js +59 -14
  247. package/src/assessments/agentAssessor.ts +26 -12
  248. package/src/assessments/assessmentStore.ts +52 -40
  249. package/src/config.ts +70 -13
  250. package/src/control/fileManager.ts +40 -0
  251. package/src/control/middleware/auth.ts +22 -4
  252. package/src/control/middleware/static.ts +16 -3
  253. package/src/control/routeTable.ts +301 -0
  254. package/src/control/routes/audit.ts +14 -6
  255. package/src/control/routes/evidence.ts +1 -1
  256. package/src/control/routes/lineage.ts +1 -1
  257. package/src/control/routes/process.ts +341 -40
  258. package/src/control/routes/sessions.ts +88 -25
  259. package/src/control/routes/status.ts +97 -17
  260. package/src/control/routes/taskActions.ts +24 -58
  261. package/src/control/routes/tasks.ts +66 -58
  262. package/src/control/routes/workspace.ts +56 -25
  263. package/src/control/runtime.ts +69 -33
  264. package/src/control/server.ts +36 -342
  265. package/src/control/shared.ts +77 -11
  266. package/src/direct/directAudit.ts +17 -9
  267. package/src/direct/directGuards.ts +72 -44
  268. package/src/direct/directPatch.ts +95 -8
  269. package/src/direct/directSessionStore.ts +211 -55
  270. package/src/direct/directVerification.ts +9 -23
  271. package/src/doctor.ts +73 -59
  272. package/src/goal/acceptanceEngine.ts +1 -1
  273. package/src/goal/goalProgress.ts +17 -13
  274. package/src/goal/goalReport.ts +5 -13
  275. package/src/goal/goalStatus.ts +19 -3
  276. package/src/goal/goalStore.ts +241 -54
  277. package/src/goal/handoffExport.ts +6 -6
  278. package/src/goal/specKitImport.ts +12 -12
  279. package/src/goal/subgoalSync.ts +11 -8
  280. package/src/goal/worktreeManager.ts +139 -22
  281. package/src/httpServer.ts +113 -30
  282. package/src/logging.ts +10 -3
  283. package/src/policy/projectPolicy.ts +5 -2
  284. package/src/release/releaseGate.ts +74 -27
  285. package/src/runner/agentInvocation.ts +126 -4
  286. package/src/runner/changeCapture.ts +291 -57
  287. package/src/runner/postTaskCleanup.ts +41 -17
  288. package/src/runner/processSecurity.ts +433 -0
  289. package/src/runner/runTask.ts +138 -91
  290. package/src/runner/simpleProcess.ts +66 -31
  291. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  292. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  293. package/src/runner/taskStatusStore.ts +54 -0
  294. package/src/runner/watch.ts +281 -51
  295. package/src/security/contentRedaction.ts +2 -2
  296. package/src/security/discoveryTokenStore.ts +18 -4
  297. package/src/security/loopbackHost.ts +19 -0
  298. package/src/security/pathGuard.ts +35 -16
  299. package/src/security/planGuard.ts +2 -1
  300. package/src/security/runtimeGuard.ts +9 -6
  301. package/src/security/secretComparison.ts +13 -0
  302. package/src/security/sensitiveGuard.ts +27 -8
  303. package/src/security/toolInvocationGuard.ts +2 -2
  304. package/src/security/workspaceRootGuard.ts +53 -0
  305. package/src/smoke-test.ts +25 -24
  306. package/src/tools/catalog/index.ts +4 -0
  307. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  308. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  309. package/src/tools/definitions/toolDefs.ts +1264 -0
  310. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  311. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
  312. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
  313. package/src/tools/diagnostics/index.ts +6 -0
  314. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  315. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  316. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  317. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  318. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  319. package/src/tools/direct/index.ts +3 -0
  320. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  321. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  322. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  323. package/src/tools/discovery/index.ts +3 -0
  324. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  325. package/src/tools/dispatch/coreDispatch.ts +36 -31
  326. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  327. package/src/tools/dispatch/directDispatch.ts +11 -10
  328. package/src/tools/dispatch/goalDispatch.ts +5 -4
  329. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  330. package/src/tools/dispatch/validation.ts +78 -0
  331. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  332. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  333. package/src/tools/goals/index.ts +3 -0
  334. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  335. package/src/tools/registry.ts +17 -1261
  336. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  337. package/src/tools/release/index.ts +2 -0
  338. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  339. package/src/tools/tasks/cancelTask.ts +109 -0
  340. package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
  341. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  342. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  343. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  344. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  345. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  346. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  347. package/src/tools/tasks/index.ts +18 -0
  348. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  349. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  350. package/src/tools/tasks/retryTask.ts +69 -0
  351. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
  352. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  353. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  354. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  355. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  356. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  357. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  358. package/src/tools/workspace/index.ts +10 -0
  359. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  360. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  361. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  362. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  363. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  364. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  365. package/src/tools/workspace/syncFile.ts +292 -0
  366. package/src/utils/atomicFile.ts +58 -0
  367. package/src/utils/boundedFile.ts +116 -0
  368. package/src/utils/lockedJsonFile.ts +251 -0
  369. package/src/utils/platform.ts +31 -0
  370. package/src/utils/stableJson.ts +12 -0
  371. package/src/version.ts +3 -2
  372. package/src/watcherStatus.ts +19 -1
  373. package/ui/desktop-bootstrap.js +11 -0
  374. package/ui/desktop-bridge.js +36 -0
  375. package/ui/desktop.css +160 -0
  376. package/ui/getting-started.js +93 -0
  377. package/ui/i18n.js +484 -0
  378. package/ui/log-parser.js +30 -0
  379. package/ui/pages/audit.html +12 -0
  380. package/ui/pages/dashboard.html +90 -147
  381. package/ui/pages/direct-sessions.html +123 -26
  382. package/ui/pages/getting-started.html +43 -0
  383. package/ui/pages/logs.html +105 -22
  384. package/ui/pages/settings.html +101 -0
  385. package/ui/pages/task-detail.html +12 -0
  386. package/ui/pages/tasks.html +54 -8
  387. package/ui/pages/workspace.html +38 -1
  388. package/ui/settings.js +245 -0
  389. package/PatchWarden-Control.cmd +0 -6
  390. package/Restart-PatchWarden-Control.cmd +0 -6
  391. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  392. package/dist/test/unit/acceptance-engine.test.js +0 -228
  393. package/dist/test/unit/agent-router.test.d.ts +0 -1
  394. package/dist/test/unit/agent-router.test.js +0 -287
  395. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  396. package/dist/test/unit/android-doctor.test.js +0 -118
  397. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  398. package/dist/test/unit/apply-patch.test.js +0 -225
  399. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  400. package/dist/test/unit/audit-checks.test.js +0 -350
  401. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  402. package/dist/test/unit/chinese-path.test.js +0 -91
  403. package/dist/test/unit/command-guard.test.d.ts +0 -1
  404. package/dist/test/unit/command-guard.test.js +0 -160
  405. package/dist/test/unit/create-task.test.d.ts +0 -1
  406. package/dist/test/unit/create-task.test.js +0 -197
  407. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  408. package/dist/test/unit/diagnose-task.test.js +0 -457
  409. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  410. package/dist/test/unit/direct-guards.test.js +0 -328
  411. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  412. package/dist/test/unit/discovery-token-store.test.js +0 -139
  413. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  414. package/dist/test/unit/evidence-pack.test.js +0 -224
  415. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  416. package/dist/test/unit/get-task-status.test.js +0 -174
  417. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  418. package/dist/test/unit/get-task-summary.test.js +0 -146
  419. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  420. package/dist/test/unit/goal-graph.test.js +0 -298
  421. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  422. package/dist/test/unit/goal-progress.test.js +0 -381
  423. package/dist/test/unit/goal-report.test.d.ts +0 -1
  424. package/dist/test/unit/goal-report.test.js +0 -159
  425. package/dist/test/unit/goal-status.test.d.ts +0 -1
  426. package/dist/test/unit/goal-status.test.js +0 -215
  427. package/dist/test/unit/goal-store.test.d.ts +0 -1
  428. package/dist/test/unit/goal-store.test.js +0 -253
  429. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  430. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  431. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  432. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  433. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  434. package/dist/test/unit/handoff-export.test.js +0 -263
  435. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  436. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  437. package/dist/test/unit/logging.test.d.ts +0 -1
  438. package/dist/test/unit/logging.test.js +0 -397
  439. package/dist/test/unit/path-guard.test.d.ts +0 -1
  440. package/dist/test/unit/path-guard.test.js +0 -133
  441. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  442. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  443. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  444. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  445. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  446. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  447. package/dist/test/unit/release-gate.test.d.ts +0 -1
  448. package/dist/test/unit/release-gate.test.js +0 -242
  449. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  450. package/dist/test/unit/run-task-loop.test.js +0 -380
  451. package/dist/test/unit/safe-status.test.d.ts +0 -1
  452. package/dist/test/unit/safe-status.test.js +0 -165
  453. package/dist/test/unit/safe-views.test.d.ts +0 -1
  454. package/dist/test/unit/safe-views.test.js +0 -171
  455. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  456. package/dist/test/unit/schema-drift-check.test.js +0 -176
  457. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  458. package/dist/test/unit/sensitive-guard.test.js +0 -104
  459. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  460. package/dist/test/unit/spec-kit-import.test.js +0 -341
  461. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  462. package/dist/test/unit/subgoal-sync.test.js +0 -183
  463. package/dist/test/unit/sync-file.test.d.ts +0 -1
  464. package/dist/test/unit/sync-file.test.js +0 -154
  465. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  466. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  467. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  468. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  469. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  470. package/dist/test/unit/toolSearch.test.js +0 -571
  471. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  472. package/dist/test/unit/wait-for-task.test.js +0 -144
  473. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  474. package/dist/test/unit/watcher-status.test.js +0 -169
  475. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  476. package/dist/test/unit/worktree-manager.test.js +0 -176
  477. package/dist/tools/applyPatch.js +0 -41
  478. package/dist/tools/cancelTask.d.ts +0 -30
  479. package/dist/tools/cancelTask.js +0 -64
  480. package/dist/tools/finalizeDirectSession.js +0 -84
  481. package/dist/tools/goalSubgoalTask.js +0 -110
  482. package/dist/tools/killTask.d.ts +0 -15
  483. package/dist/tools/retryTask.js +0 -45
  484. package/dist/tools/runVerification.js +0 -36
  485. package/dist/tools/syncFile.js +0 -65
  486. package/docs/control-center/control-center-daily-driver.md +0 -211
  487. package/docs/control-center/control-center-mvp.md +0 -205
  488. package/docs/control-center/control-center-phase2.md +0 -159
  489. package/docs/execution-plan-2026-07-09.md +0 -315
  490. package/docs/release-v0.6.0.md +0 -71
  491. package/docs/release-v0.6.1.md +0 -75
  492. package/docs/release-v0.6.4.md +0 -45
  493. package/docs/roadmap-execution-and-acceptance.md +0 -365
  494. package/src/test/unit/acceptance-engine.test.ts +0 -261
  495. package/src/test/unit/agent-router.test.ts +0 -342
  496. package/src/test/unit/android-doctor.test.ts +0 -158
  497. package/src/test/unit/apply-patch.test.ts +0 -293
  498. package/src/test/unit/audit-checks.test.ts +0 -567
  499. package/src/test/unit/chinese-path.test.ts +0 -106
  500. package/src/test/unit/command-guard.test.ts +0 -221
  501. package/src/test/unit/create-task.test.ts +0 -255
  502. package/src/test/unit/diagnose-task.test.ts +0 -544
  503. package/src/test/unit/direct-guards.test.ts +0 -467
  504. package/src/test/unit/discovery-token-store.test.ts +0 -181
  505. package/src/test/unit/evidence-pack.test.ts +0 -251
  506. package/src/test/unit/get-task-status.test.ts +0 -203
  507. package/src/test/unit/get-task-summary.test.ts +0 -173
  508. package/src/test/unit/goal-graph.test.ts +0 -347
  509. package/src/test/unit/goal-progress.test.ts +0 -538
  510. package/src/test/unit/goal-report.test.ts +0 -189
  511. package/src/test/unit/goal-status.test.ts +0 -270
  512. package/src/test/unit/goal-store.test.ts +0 -318
  513. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  514. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  515. package/src/test/unit/handoff-export.test.ts +0 -295
  516. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  517. package/src/test/unit/logging.test.ts +0 -497
  518. package/src/test/unit/path-guard.test.ts +0 -180
  519. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  520. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  521. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  522. package/src/test/unit/release-gate.test.ts +0 -314
  523. package/src/test/unit/run-task-loop.test.ts +0 -425
  524. package/src/test/unit/safe-status.test.ts +0 -187
  525. package/src/test/unit/safe-views.test.ts +0 -184
  526. package/src/test/unit/schema-drift-check.test.ts +0 -260
  527. package/src/test/unit/sensitive-guard.test.ts +0 -124
  528. package/src/test/unit/spec-kit-import.test.ts +0 -429
  529. package/src/test/unit/subgoal-sync.test.ts +0 -236
  530. package/src/test/unit/sync-file.test.ts +0 -231
  531. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  532. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  533. package/src/test/unit/toolSearch.test.ts +0 -756
  534. package/src/test/unit/wait-for-task.test.ts +0 -176
  535. package/src/test/unit/watcher-status.test.ts +0 -190
  536. package/src/test/unit/worktree-manager.test.ts +0 -247
  537. package/src/tools/cancelTask.ts +0 -70
  538. package/src/tools/retryTask.ts +0 -53
  539. package/src/tools/syncFile.ts +0 -122
  540. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  541. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  542. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  543. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  544. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  545. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  546. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  547. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  548. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  549. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  550. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  551. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  552. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  553. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  554. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  555. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  556. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  557. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  558. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  559. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  560. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  561. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  562. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  563. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  564. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  565. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  566. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  567. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  568. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  569. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  570. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  571. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  572. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  573. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- cpSync,
3
+ copyFileSync,
4
4
  existsSync,
5
5
  mkdirSync,
6
6
  readFileSync,
@@ -16,7 +16,8 @@ import { fileURLToPath } from "node:url";
16
16
 
17
17
  const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
18
18
  const root = resolve(scriptDir, "..", "..");
19
- const releaseDir = resolve(root, "release");
19
+ const releaseRoot = resolve(root, "release");
20
+ const releaseDir = resolve(releaseRoot, "package");
20
21
  const archivePath = resolve(root, "patchwarden-release.tar.gz");
21
22
 
22
23
  // Read version from package.json for zip naming
@@ -31,11 +32,6 @@ const include = [
31
32
  "src",
32
33
  "ui",
33
34
  "PatchWarden.cmd",
34
- "PatchWarden-Control.cmd",
35
- "PatchWarden-Control-Tray.cmd",
36
- "PatchWarden-Desktop.cmd",
37
- "Restart-PatchWarden-Control.cmd",
38
- "Stop-PatchWarden.cmd",
39
35
  ".gitignore",
40
36
  "LICENSE",
41
37
  "README.md",
@@ -57,6 +53,8 @@ const forbidden = [
57
53
  /(^|[\\/])\.env$/,
58
54
  /\.log$/,
59
55
  /^docs[\\/]optimization-proposal\.md$/i,
56
+ /^docs[\\/]archive(?:[\\/]|$)/i,
57
+ /^(?:dist|src)[\\/]test(?:[\\/]|$)/i,
60
58
  /(^|[\\/])kill-patchwarden\.(cmd|ps1)$/i,
61
59
  ];
62
60
 
@@ -173,9 +171,15 @@ function readZipEntryNames(zipPath) {
173
171
  return names;
174
172
  }
175
173
 
174
+ const stageRelativeToRoot = toPosix(relative(root, releaseDir));
175
+ if (stageRelativeToRoot !== "release/package") {
176
+ throw new Error(`Refusing to clean non-isolated release staging path: ${stageRelativeToRoot}`);
177
+ }
178
+
176
179
  console.log("[pack-clean] Preparing clean release directory...");
177
180
  rmSync(releaseDir, { recursive: true, force: true });
178
181
  rmSync(archivePath, { force: true });
182
+ mkdirSync(releaseRoot, { recursive: true });
179
183
  mkdirSync(releaseDir, { recursive: true });
180
184
 
181
185
  for (const item of include) {
@@ -185,17 +189,26 @@ for (const item of include) {
185
189
  console.log(` skip missing: ${item}`);
186
190
  continue;
187
191
  }
188
- cpSync(source, target, {
189
- recursive: true,
190
- filter(sourcePath) {
191
- const rel = toPosix(relative(root, sourcePath));
192
- return !isForbidden(rel);
193
- },
194
- });
192
+ copyAllowedEntry(source, target);
195
193
  console.log(` copied: ${item}`);
196
194
  }
197
195
 
196
+ // Keep an independent second boundary before the fail-closed validation. This
197
+ // protects package contents if a future copy implementation changes behavior.
198
+ pruneForbiddenEntries(releaseDir);
199
+
198
200
  const releaseFiles = listFiles(releaseDir);
201
+ const requiredReleaseEntries = ["package.json", "README.md", "dist/index.js"];
202
+ const missingReleaseEntries = requiredReleaseEntries.filter(
203
+ (entry) => !existsSync(resolve(releaseDir, entry))
204
+ );
205
+ if (releaseFiles.length === 0 || missingReleaseEntries.length > 0) {
206
+ console.error("[pack-clean] Release directory is incomplete:");
207
+ for (const entry of missingReleaseEntries) {
208
+ console.error(` missing: ${entry}`);
209
+ }
210
+ process.exit(1);
211
+ }
199
212
  const badReleaseEntries = releaseFiles.filter((file) =>
200
213
  isForbidden(toPosix(relative(releaseDir, file)))
201
214
  );
@@ -296,6 +309,38 @@ function listFiles(dir) {
296
309
  return entries;
297
310
  }
298
311
 
312
+ function copyAllowedEntry(source, target) {
313
+ const rel = toPosix(relative(root, source));
314
+ if (isForbidden(rel)) {
315
+ return;
316
+ }
317
+
318
+ const stat = statSync(source);
319
+ if (stat.isDirectory()) {
320
+ mkdirSync(target, { recursive: true });
321
+ for (const name of readdirSync(source)) {
322
+ copyAllowedEntry(join(source, name), join(target, name));
323
+ }
324
+ return;
325
+ }
326
+
327
+ copyFileSync(source, target);
328
+ }
329
+
330
+ function pruneForbiddenEntries(dir) {
331
+ for (const name of readdirSync(dir)) {
332
+ const fullPath = join(dir, name);
333
+ const rel = toPosix(relative(releaseDir, fullPath));
334
+ if (isForbidden(rel)) {
335
+ rmSync(fullPath, { recursive: true, force: true });
336
+ continue;
337
+ }
338
+ if (statSync(fullPath).isDirectory()) {
339
+ pruneForbiddenEntries(fullPath);
340
+ }
341
+ }
342
+ }
343
+
299
344
  function isForbidden(value) {
300
345
  return forbidden.some((pattern) => pattern.test(value));
301
346
  }
@@ -1,10 +1,11 @@
1
- import { writeFileSync } from "node:fs";
2
- import { resolve, relative, isAbsolute } from "node:path";
1
+ import { resolve, relative, isAbsolute, sep } from "node:path";
3
2
  import { PatchWardenConfig } from "../config.js";
4
3
  import { buildAgentInvocation, buildAssessmentPrompt } from "../runner/agentInvocation.js";
5
4
  import { runSimpleProcessSync } from "../runner/simpleProcess.js";
6
5
  import { captureRepoSnapshot, compareSnapshots, type RepoSnapshot, type ChangedFile } from "../runner/changeCapture.js";
7
6
  import type { AgentAssessmentOutput, AgentAssessmentSummary } from "./assessmentStore.js";
7
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
8
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
8
9
 
9
10
  const ASSESSMENT_MARKER = "===ASSESSMENT_JSON===";
10
11
 
@@ -45,9 +46,11 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
45
46
  };
46
47
 
47
48
  // ── 1. Build prompt and write to file for {prompt_file} support ──
48
- const assessmentPrompt = buildAssessmentPrompt(input.goal, input.planContent, input.repoPath);
49
+ const safeGoal = redactSensitiveContent(input.goal).content.slice(0, 10_000);
50
+ const safePlan = redactSensitiveContent(input.planContent).content.slice(0, 1024 * 1024);
51
+ const assessmentPrompt = buildAssessmentPrompt(safeGoal, safePlan, input.repoPath);
49
52
  const promptFilePath = resolve(input.assessmentDir, "agent-assessment-prompt.md");
50
- writeFileSync(promptFilePath, assessmentPrompt, "utf-8");
53
+ atomicWriteFileSync(promptFilePath, assessmentPrompt);
51
54
  logPaths.prompt = promptFilePath;
52
55
 
53
56
  // ── 2. Before snapshot (repo-scoped) ──
@@ -89,6 +92,8 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
89
92
  maxStderrBytes: Math.max(16384, Math.floor(input.maxOutputBytes / 4)),
90
93
  stdoutPath: logPaths.stdout,
91
94
  stderrPath: logPaths.stderr,
95
+ environmentVariableNames: invocation.environmentVariableNames,
96
+ blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
92
97
  });
93
98
 
94
99
  // ── 5. After snapshot + read-only violation check ──
@@ -123,11 +128,11 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
123
128
  stderr_truncated: result.stderrTruncated,
124
129
  log_paths: { ...logPaths, violation: resolve(input.assessmentDir, "agent-assessment-violation.json") },
125
130
  };
126
- writeFileSync(summary.log_paths.violation!, JSON.stringify({
131
+ atomicWriteJsonFileSync(summary.log_paths.violation!, {
127
132
  violation: "read_only_violation",
128
133
  files: violationFiles,
129
134
  exit_code: result.exitCode,
130
- }, null, 2), "utf-8");
135
+ });
131
136
  writeSummary(logPaths.assessment, summary);
132
137
  return summary;
133
138
  }
@@ -253,12 +258,16 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
253
258
  const jsonEnd = jsonText.lastIndexOf("}");
254
259
  if (jsonStart < 0 || jsonEnd < 0 || jsonEnd <= jsonStart) return { output: null, sanitized_reasons };
255
260
 
256
- let parsed: any;
261
+ let parsedValue: unknown;
257
262
  try {
258
- parsed = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
263
+ parsedValue = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
259
264
  } catch {
260
265
  return { output: null, sanitized_reasons };
261
266
  }
267
+ if (!parsedValue || typeof parsedValue !== "object" || Array.isArray(parsedValue)) {
268
+ return { output: null, sanitized_reasons };
269
+ }
270
+ const parsed = parsedValue as Record<string, unknown>;
262
271
 
263
272
  // ── Strict validation ──
264
273
  if (parsed.risk_level !== "low" && parsed.risk_level !== "medium" && parsed.risk_level !== "high") {
@@ -272,7 +281,9 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
272
281
 
273
282
  if (!Array.isArray(parsed.reason_codes)) return { output: null, sanitized_reasons };
274
283
  if (parsed.reason_codes.length > 50) return { output: null, sanitized_reasons };
275
- const reasonCodes = parsed.reason_codes.filter((r: any) => typeof r === "string" && r.length <= 100);
284
+ const reasonCodes = parsed.reason_codes.filter((reason): reason is string =>
285
+ typeof reason === "string" && reason.length <= 100
286
+ );
276
287
  if (reasonCodes.length !== parsed.reason_codes.length) {
277
288
  sanitized_reasons.push("reason_codes_filtered");
278
289
  }
@@ -287,7 +298,10 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
287
298
  if (isAbsolute(p)) { pathsSanitized = true; continue; }
288
299
  const resolved = resolve(repoPath, p);
289
300
  const rel = relative(repoPath, resolved);
290
- if (rel.startsWith("..")) { pathsSanitized = true; continue; }
301
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
302
+ pathsSanitized = true;
303
+ continue;
304
+ }
291
305
  affectedPaths.push(p);
292
306
  }
293
307
  if (pathsSanitized) {
@@ -297,7 +311,7 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
297
311
  if (!Array.isArray(parsed.destructive_actions)) return { output: null, sanitized_reasons };
298
312
  if (parsed.destructive_actions.length > 20) return { output: null, sanitized_reasons };
299
313
  const destructiveActions = parsed.destructive_actions.filter(
300
- (d: any) => typeof d === "string" && d.length <= 200
314
+ (action): action is string => typeof action === "string" && action.length <= 200
301
315
  );
302
316
 
303
317
  if (typeof parsed.requires_user_confirm !== "boolean") return { output: null, sanitized_reasons };
@@ -320,5 +334,5 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
320
334
  }
321
335
 
322
336
  function writeSummary(path: string, summary: AgentAssessmentSummary): void {
323
- writeFileSync(path, JSON.stringify(summary, null, 2), "utf-8");
337
+ atomicWriteJsonFileSync(path, summary);
324
338
  }
@@ -1,17 +1,18 @@
1
1
  import {
2
2
  mkdirSync,
3
- writeFileSync,
4
3
  existsSync,
5
4
  readFileSync,
6
- renameSync,
7
5
  } from "node:fs";
8
6
  import { createHash, randomBytes } from "node:crypto";
9
7
  import { resolve, join } from "node:path";
10
8
  import { getAssessmentsDir, getConfig, getRepoAllowedTestCommands, PatchWardenConfig } from "../config.js";
11
9
  import { guardPath } from "../security/pathGuard.js";
12
10
  import { PatchWardenError } from "../errors.js";
11
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
12
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
13
+ import { withFileLockSync } from "../utils/lockedJsonFile.js";
13
14
  import { TOOL_SCHEMA_EPOCH } from "../version.js";
14
- import { getLastToolCatalogSnapshot } from "../tools/toolCatalog.js";
15
+ import { getLastToolCatalogSnapshot } from "../tools/catalog/toolCatalog.js";
15
16
  import { captureRepoSnapshot, type RepoSnapshot } from "../runner/changeCapture.js";
16
17
  import type { RiskAssessmentResult } from "../security/riskEngine.js";
17
18
  import type { TaskTemplateName, ChangePolicy } from "../tools/taskTemplates.js";
@@ -203,7 +204,7 @@ export function createAssessment(input: AssessmentCreateInput): AssessmentRecord
203
204
  repo_path: input.repo_path,
204
205
  resolved_repo_path: input.resolved_repo_path,
205
206
  template: input.template || null,
206
- goal: input.goal || null,
207
+ goal: input.goal ? redactSensitiveContent(input.goal).content : null,
207
208
  test_command: input.test_command || null,
208
209
  verify_commands: input.verify_commands || [],
209
210
  agent: input.agent,
@@ -216,7 +217,7 @@ export function createAssessment(input: AssessmentCreateInput): AssessmentRecord
216
217
  agent_assessment_summary: input.agent_assessment_summary || null,
217
218
  };
218
219
 
219
- writeFileSync(join(dir, "assessment.json"), JSON.stringify(record, null, 2), "utf-8");
220
+ atomicWriteJsonFileSync(join(dir, "assessment.json"), record);
220
221
 
221
222
  return record;
222
223
  }
@@ -323,6 +324,10 @@ export async function confirmAssessment(assessmentId: string): Promise<Assessmen
323
324
  );
324
325
  }
325
326
 
327
+ const config = getConfig();
328
+ const assessmentFile = resolve(getAssessmentsDir(config), assessmentId, "assessment.json");
329
+ guardPath(assessmentFile, config.workspaceRoot, config.assessmentsDir);
330
+
326
331
  const assessment = readAssessment(assessmentId);
327
332
  if (assessment.decision !== "needs_confirm" || !assessment.requires_confirm) {
328
333
  throw new PatchWardenError(
@@ -335,43 +340,50 @@ export async function confirmAssessment(assessmentId: string): Promise<Assessmen
335
340
  }
336
341
 
337
342
  const snapshot = await captureRepoSnapshot(assessment.resolved_repo_path);
338
- const validation = validateAssessmentFreshness(assessmentId, snapshot, {
339
- allow_unconfirmed: true,
340
- // A standalone CLI has no active MCP profile snapshot. The execute path
341
- // still performs the full tool-manifest check before creating the task.
342
- skip_tool_manifest: true,
343
- });
344
- if (!validation.valid || !validation.assessment) {
345
- throw new PatchWardenError(
346
- validation.failure_reason || "assessment_validation_failed",
347
- `Assessment "${assessmentId}" cannot be confirmed: ${validation.failure_reason || "validation_failed"}.`,
348
- "Run create_task with execution_mode=assess_only again, then confirm the new assessment locally."
349
- );
350
- }
351
343
 
352
- const confirmedAt = validation.assessment.confirmed_at || new Date().toISOString();
353
- const confirmedRecord: AssessmentRecord = {
354
- ...validation.assessment,
355
- confirmed: true,
356
- confirmed_at: confirmedAt,
357
- confirm_code: validation.assessment.confirm_code || randomBytes(8).toString("hex"),
358
- };
359
- const config = getConfig();
360
- const assessmentFile = resolve(getAssessmentsDir(config), assessmentId, "assessment.json");
361
- guardPath(assessmentFile, config.workspaceRoot, config.assessmentsDir);
362
- const temporaryFile = `${assessmentFile}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
363
- guardPath(temporaryFile, config.workspaceRoot, config.assessmentsDir);
364
- writeFileSync(temporaryFile, JSON.stringify(confirmedRecord, null, 2), "utf-8");
365
- renameSync(temporaryFile, assessmentFile);
344
+ // Re-read and write while holding one cross-process lock. Concurrent local
345
+ // confirmers may compute snapshots in parallel, but they cannot replace one
346
+ // another's confirmation metadata from a stale assessment record.
347
+ return withFileLockSync(assessmentFile, () => {
348
+ const validation = validateAssessmentFreshness(assessmentId, snapshot, {
349
+ allow_unconfirmed: true,
350
+ // A standalone CLI has no active MCP profile snapshot. The execute path
351
+ // still performs the full tool-manifest check before creating the task.
352
+ skip_tool_manifest: true,
353
+ });
354
+ if (!validation.valid || !validation.assessment) {
355
+ throw new PatchWardenError(
356
+ validation.failure_reason || "assessment_validation_failed",
357
+ `Assessment "${assessmentId}" cannot be confirmed: ${validation.failure_reason || "validation_failed"}.`,
358
+ "Run create_task with execution_mode=assess_only again, then confirm the new assessment locally."
359
+ );
360
+ }
361
+ if (validation.assessment.decision !== "needs_confirm" || !validation.assessment.requires_confirm) {
362
+ throw new PatchWardenError(
363
+ "assessment_confirmation_not_allowed",
364
+ `Assessment "${assessmentId}" is no longer eligible for local confirmation.`,
365
+ "Run create_task with execution_mode=assess_only again."
366
+ );
367
+ }
366
368
 
367
- return {
368
- assessment_id: assessmentId,
369
- decision: "needs_confirm",
370
- confirmed: true,
371
- confirmed_at: confirmedAt,
372
- expires_at: confirmedRecord.expires_at,
373
- next_action: "Call create_task with only execution_mode=execute and this full assessment_id.",
374
- };
369
+ const confirmedAt = validation.assessment.confirmed_at || new Date().toISOString();
370
+ const confirmedRecord: AssessmentRecord = {
371
+ ...validation.assessment,
372
+ confirmed: true,
373
+ confirmed_at: confirmedAt,
374
+ confirm_code: validation.assessment.confirm_code || randomBytes(8).toString("hex"),
375
+ };
376
+ atomicWriteJsonFileSync(assessmentFile, confirmedRecord);
377
+
378
+ return {
379
+ assessment_id: assessmentId,
380
+ decision: "needs_confirm",
381
+ confirmed: true,
382
+ confirmed_at: confirmedAt,
383
+ expires_at: confirmedRecord.expires_at,
384
+ next_action: "Call create_task with only execution_mode=execute and this full assessment_id.",
385
+ };
386
+ });
375
387
  }
376
388
 
377
389
  export function computeWorkspaceFingerprint(snapshot: RepoSnapshot): string {
package/src/config.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
- import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { validateWorkspaceRoot } from "./security/workspaceRootGuard.js";
3
4
 
4
5
  // ── Type definitions ──────────────────────────────────────────────
5
6
 
6
7
  export interface AgentConfig {
7
8
  command: string;
8
9
  args: string[];
10
+ envAllowlist?: string[];
11
+ adapter?: string;
12
+ model?: string;
9
13
  }
10
14
 
11
15
  export interface PatchWardenConfig {
@@ -31,6 +35,12 @@ export interface PatchWardenConfig {
31
35
  agentAssessmentMaxOutputBytes?: number;
32
36
  agentAssessmentAgentName?: string;
33
37
  enableDirectProfile?: boolean;
38
+ tunnelClientPath?: string;
39
+ tunnelProxy?: {
40
+ scope: "shared" | "separate";
41
+ core: { mode: "environment" | "none" | "manual"; url?: string };
42
+ direct: { mode: "environment" | "none" | "manual"; url?: string };
43
+ };
34
44
  directSessionsDir: string;
35
45
  directSessionTtlSeconds: number;
36
46
  directMaxPatchBytes: number;
@@ -47,16 +57,7 @@ const DEFAULT_CONFIG: PatchWardenConfig = {
47
57
  tasksDir: ".patchwarden/tasks",
48
58
  assessmentsDir: ".patchwarden/assessments",
49
59
  assessmentTtlSeconds: 3600,
50
- agents: {
51
- codex: {
52
- command: "codex",
53
- args: ["exec", "--cd", "{repo}", "{prompt}"],
54
- },
55
- opencode: {
56
- command: "opencode",
57
- args: ["run", "{prompt}"],
58
- },
59
- },
60
+ agents: {},
60
61
  allowedTestCommands: [
61
62
  "npm test",
62
63
  "npm run test",
@@ -175,6 +176,11 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
175
176
  if (!config.workspaceRoot || typeof config.workspaceRoot !== "string") {
176
177
  throw new Error("workspaceRoot must be a non-empty string");
177
178
  }
179
+ const workspaceValidation = validateWorkspaceRoot(config.workspaceRoot);
180
+ if (!workspaceValidation.ok) {
181
+ throw new Error(`Invalid workspaceRoot: ${workspaceValidation.reason} (${workspaceValidation.path})`);
182
+ }
183
+ config.workspaceRoot = workspaceValidation.path;
178
184
  if (!config.plansDir || typeof config.plansDir !== "string") {
179
185
  throw new Error("plansDir must be a non-empty string");
180
186
  }
@@ -190,6 +196,20 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
190
196
  if (!config.agents || typeof config.agents !== "object") {
191
197
  throw new Error("agents must be an object");
192
198
  }
199
+ const agents: Record<string, AgentConfig> = {};
200
+ for (const [agentName, agent] of Object.entries(config.agents)) {
201
+ if (!agent || typeof agent !== "object" || typeof agent.command !== "string" || !Array.isArray(agent.args)) {
202
+ throw new Error(`agents["${agentName}"] must define command and args`);
203
+ }
204
+ const envAllowlist = agent.envAllowlist ?? [];
205
+ if (!Array.isArray(envAllowlist) || envAllowlist.some((name) => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) {
206
+ throw new Error(`agents["${agentName}"].envAllowlist must contain only environment variable names`);
207
+ }
208
+ const blockedNames = new Set(["CONTROL_PLANE_API_KEY", "PATCHWARDEN_OWNER_TOKEN", config.http?.ownerTokenEnv?.toUpperCase()]);
209
+ const blocked = envAllowlist.find((name) => blockedNames.has(name.toUpperCase()));
210
+ if (blocked) throw new Error(`agents["${agentName}"].envAllowlist cannot include reserved variable "${blocked}"`);
211
+ agents[agentName] = { ...agent, args: [...agent.args], envAllowlist: [...new Set(envAllowlist)] };
212
+ }
193
213
  if (!Array.isArray(config.allowedTestCommands)) {
194
214
  throw new Error("allowedTestCommands must be an array");
195
215
  }
@@ -263,8 +283,14 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
263
283
  if (config.http.host !== undefined && typeof config.http.host !== "string") {
264
284
  throw new Error("http.host must be a string");
265
285
  }
266
- if (config.http.ownerTokenEnv !== undefined && typeof config.http.ownerTokenEnv !== "string") {
267
- throw new Error("http.ownerTokenEnv must be a string");
286
+ if (
287
+ config.http.ownerTokenEnv !== undefined
288
+ && (
289
+ typeof config.http.ownerTokenEnv !== "string"
290
+ || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.http.ownerTokenEnv)
291
+ )
292
+ ) {
293
+ throw new Error("http.ownerTokenEnv must be a valid environment variable name");
268
294
  }
269
295
  }
270
296
  if (config.enableRunTaskTool !== undefined && typeof config.enableRunTaskTool !== "boolean") {
@@ -289,6 +315,36 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
289
315
  if (config.enableDirectProfile !== undefined && typeof config.enableDirectProfile !== "boolean") {
290
316
  throw new Error("enableDirectProfile must be a boolean");
291
317
  }
318
+ if (config.tunnelClientPath !== undefined) {
319
+ if (typeof config.tunnelClientPath !== "string" || !isAbsolute(config.tunnelClientPath)) {
320
+ throw new Error("tunnelClientPath must be an absolute path");
321
+ }
322
+ if (basename(config.tunnelClientPath).toLowerCase() !== "tunnel-client.exe") {
323
+ throw new Error("tunnelClientPath must point to tunnel-client.exe");
324
+ }
325
+ }
326
+ if (config.tunnelProxy !== undefined) {
327
+ if (!config.tunnelProxy || typeof config.tunnelProxy !== "object" || !["shared", "separate"].includes(config.tunnelProxy.scope)) {
328
+ throw new Error("tunnelProxy.scope must be shared or separate");
329
+ }
330
+ for (const key of ["core", "direct"] as const) {
331
+ const endpoint = config.tunnelProxy[key];
332
+ if (!endpoint || typeof endpoint !== "object" || !["environment", "none", "manual"].includes(endpoint.mode)) {
333
+ throw new Error(`tunnelProxy.${key}.mode must be environment, none, or manual`);
334
+ }
335
+ if (endpoint.mode === "manual") {
336
+ if (typeof endpoint.url !== "string") throw new Error(`tunnelProxy.${key}.url is required in manual mode`);
337
+ let parsed: URL;
338
+ try { parsed = new URL(endpoint.url); } catch { throw new Error(`tunnelProxy.${key}.url must be a valid URL`); }
339
+ if (!["http:", "https:", "socks5:"].includes(parsed.protocol)) {
340
+ throw new Error(`tunnelProxy.${key}.url must use http, https, or socks5`);
341
+ }
342
+ if (parsed.username || parsed.password) throw new Error(`tunnelProxy.${key}.url must not contain credentials`);
343
+ } else if (endpoint.url !== undefined) {
344
+ throw new Error(`tunnelProxy.${key}.url is only allowed in manual mode`);
345
+ }
346
+ }
347
+ }
292
348
  if (!config.directSessionsDir || typeof config.directSessionsDir !== "string") {
293
349
  throw new Error("directSessionsDir must be a non-empty string");
294
350
  }
@@ -331,6 +387,7 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
331
387
 
332
388
  return {
333
389
  ...config,
390
+ agents,
334
391
  workspaceRoot: resolve(config.workspaceRoot),
335
392
  allowedTestCommands: [...new Set(config.allowedTestCommands.map((command) => command.trim()))],
336
393
  repoAllowedTestCommands,
@@ -0,0 +1,40 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { buildChildEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
4
+
5
+ const FILE_MANAGER_ENVIRONMENT = [
6
+ "DISPLAY",
7
+ "WAYLAND_DISPLAY",
8
+ "DBUS_SESSION_BUS_ADDRESS",
9
+ "XDG_RUNTIME_DIR",
10
+ ] as const;
11
+
12
+ /** Launch a trusted platform file manager without inheriting ambient secrets. */
13
+ export function launchFileManager(target: string, cwd: string): boolean {
14
+ try {
15
+ const env = buildChildEnvironment({ cwd, allowedNames: FILE_MANAGER_ENVIRONMENT });
16
+ const requested = fileManagerCommand(env);
17
+ const command = resolveTrustedExecutable(requested, cwd, { pathValue: env.PATH });
18
+ const child = spawn(command, [target], {
19
+ cwd,
20
+ detached: true,
21
+ env,
22
+ stdio: "ignore",
23
+ windowsHide: true,
24
+ });
25
+ child.on("error", () => { /* opening a convenience viewer is best effort */ });
26
+ child.unref();
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function fileManagerCommand(env: NodeJS.ProcessEnv): string {
34
+ if (process.platform === "win32") {
35
+ const windowsRoot = env.SystemRoot || env.WINDIR;
36
+ return windowsRoot ? join(windowsRoot, "explorer.exe") : "explorer.exe";
37
+ }
38
+ if (process.platform === "darwin") return "open";
39
+ return "xdg-open";
40
+ }
@@ -7,11 +7,29 @@
7
7
  * action must pass through `checkControlToken` first.
8
8
  */
9
9
  import { type IncomingMessage } from "node:http";
10
+ import { firstHeaderValue, timingSafeStringEqual } from "../../security/secretComparison.js";
11
+ import { isTrustedLoopbackHostHeader } from "../../security/loopbackHost.js";
10
12
  import { controlToken } from "../shared.js";
11
13
 
12
14
  export function checkControlToken(req: IncomingMessage): boolean {
13
- const header = req.headers["x-patchwarden-control-token"];
14
- const provided = Array.isArray(header) ? header[0] : header;
15
- if (typeof provided !== "string" || provided.length === 0) return false;
16
- return provided === controlToken;
15
+ const provided = firstHeaderValue(req.headers["x-patchwarden-control-token"]);
16
+ return provided.length > 0 && timingSafeStringEqual(provided, controlToken);
17
+ }
18
+
19
+ /**
20
+ * Reject non-loopback Host headers before exposing the browser-readable
21
+ * control token. Binding to 127.0.0.1 alone does not prevent DNS rebinding.
22
+ */
23
+ export function isTrustedControlHost(
24
+ req: IncomingMessage,
25
+ expectedPort: number,
26
+ ): boolean {
27
+ return isTrustedControlHostHeader(req.headers.host, expectedPort);
28
+ }
29
+
30
+ export function isTrustedControlHostHeader(
31
+ value: string | string[] | undefined,
32
+ expectedPort: number,
33
+ ): boolean {
34
+ return isTrustedLoopbackHostHeader(value, expectedPort);
17
35
  }
@@ -5,7 +5,7 @@
5
5
  * assets, css) with path-traversal protection, plus the inline SVG favicon.
6
6
  * Content types are mapped from `CONTENT_TYPES` in shared.ts.
7
7
  */
8
- import { existsSync, readFileSync, statSync } from "node:fs";
8
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
9
9
  import { extname, isAbsolute, relative, resolve } from "node:path";
10
10
  import { type ServerResponse } from "node:http";
11
11
  import {
@@ -51,9 +51,22 @@ export function serveStatic(res: ServerResponse, urlPath: string): void {
51
51
  sendJson(res, 404, { error: "Not found" });
52
52
  return;
53
53
  }
54
- const ext = extname(candidate).toLowerCase();
54
+ // Resolve links before reading. Lexical `..` checks alone do not stop a
55
+ // symlink or junction inside `ui/` from pointing outside the bundle.
56
+ const realRoot = realpathSync(uiRoot);
57
+ const realCandidate = realpathSync(candidate);
58
+ const realRelative = relative(realRoot, realCandidate);
59
+ if (realRelative === "" || realRelative.startsWith("..") || isAbsolute(realRelative)) {
60
+ sendJson(res, 403, { error: "Forbidden" });
61
+ return;
62
+ }
63
+ if (!statSync(realCandidate).isFile()) {
64
+ sendJson(res, 404, { error: "Not found" });
65
+ return;
66
+ }
67
+ const ext = extname(realCandidate).toLowerCase();
55
68
  const contentType = CONTENT_TYPES[ext] || "application/octet-stream";
56
- const content = readFileSync(candidate);
69
+ const content = readFileSync(realCandidate);
57
70
  res.writeHead(200, { "Content-Type": contentType });
58
71
  res.end(content);
59
72
  } catch (err) {