patchwarden 1.5.0 → 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 (597) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +194 -26
  3. package/README.md +160 -22
  4. package/dist/assessments/agentAssessor.d.ts +1 -1
  5. package/dist/assessments/agentAssessor.js +24 -15
  6. package/dist/assessments/assessmentStore.d.ts +1 -1
  7. package/dist/assessments/assessmentStore.js +44 -36
  8. package/dist/assessments/confirmCli.js +5 -4
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +98 -11
  11. package/dist/control/fileManager.d.ts +2 -0
  12. package/dist/control/fileManager.js +39 -0
  13. package/dist/control/middleware/auth.d.ts +16 -0
  14. package/dist/control/middleware/auth.js +17 -0
  15. package/dist/control/middleware/static.d.ts +3 -0
  16. package/dist/control/middleware/static.js +78 -0
  17. package/dist/control/routeTable.d.ts +24 -0
  18. package/dist/control/routeTable.js +263 -0
  19. package/dist/control/routes/audit.d.ts +15 -0
  20. package/dist/control/routes/audit.js +287 -0
  21. package/dist/control/routes/evidence.d.ts +4 -0
  22. package/dist/control/routes/evidence.js +96 -0
  23. package/dist/control/routes/lineage.d.ts +3 -0
  24. package/dist/control/routes/lineage.js +71 -0
  25. package/dist/control/routes/policy.d.ts +3 -0
  26. package/dist/control/routes/policy.js +81 -0
  27. package/dist/control/routes/process.d.ts +12 -0
  28. package/dist/control/routes/process.js +487 -0
  29. package/dist/control/routes/sessions.d.ts +30 -0
  30. package/dist/control/routes/sessions.js +287 -0
  31. package/dist/control/routes/status.d.ts +51 -0
  32. package/dist/control/routes/status.js +307 -0
  33. package/dist/control/routes/taskActions.d.ts +21 -0
  34. package/dist/control/routes/taskActions.js +197 -0
  35. package/dist/control/routes/tasks.d.ts +24 -0
  36. package/dist/control/routes/tasks.js +309 -0
  37. package/dist/control/routes/workspace.d.ts +15 -0
  38. package/dist/control/routes/workspace.js +220 -0
  39. package/dist/control/runtime.d.ts +92 -0
  40. package/dist/control/runtime.js +425 -0
  41. package/dist/control/server.d.ts +13 -0
  42. package/dist/control/server.js +150 -0
  43. package/dist/control/shared.d.ts +59 -0
  44. package/dist/control/shared.js +341 -0
  45. package/dist/controlCenter.d.ts +6 -0
  46. package/dist/controlCenter.js +7 -2197
  47. package/dist/direct/directAudit.js +234 -225
  48. package/dist/direct/directGuards.d.ts +2 -1
  49. package/dist/direct/directGuards.js +72 -37
  50. package/dist/direct/directPatch.d.ts +6 -1
  51. package/dist/direct/directPatch.js +55 -8
  52. package/dist/direct/directSessionStore.d.ts +16 -6
  53. package/dist/direct/directSessionStore.js +127 -35
  54. package/dist/direct/directVerification.js +8 -16
  55. package/dist/doctor.d.ts +18 -1
  56. package/dist/doctor.js +614 -373
  57. package/dist/goal/acceptanceEngine.d.ts +1 -1
  58. package/dist/goal/goalProgress.js +75 -69
  59. package/dist/goal/goalReport.d.ts +54 -0
  60. package/dist/goal/goalReport.js +197 -0
  61. package/dist/goal/goalStatus.d.ts +8 -0
  62. package/dist/goal/goalStatus.js +10 -3
  63. package/dist/goal/goalStore.d.ts +9 -2
  64. package/dist/goal/goalStore.js +152 -45
  65. package/dist/goal/handoffExport.js +6 -6
  66. package/dist/goal/specKitImport.d.ts +63 -0
  67. package/dist/goal/specKitImport.js +221 -0
  68. package/dist/goal/subgoalSync.js +13 -10
  69. package/dist/goal/worktreeManager.d.ts +2 -0
  70. package/dist/goal/worktreeManager.js +209 -136
  71. package/dist/httpServer.js +126 -44
  72. package/dist/index.js +7 -4
  73. package/dist/logging.d.ts +7 -1
  74. package/dist/logging.js +13 -3
  75. package/dist/policy/projectPolicy.js +5 -2
  76. package/dist/release/releaseGate.d.ts +1 -1
  77. package/dist/release/releaseGate.js +78 -25
  78. package/dist/runner/agentInvocation.d.ts +14 -0
  79. package/dist/runner/agentInvocation.js +96 -4
  80. package/dist/runner/changeCapture.d.ts +14 -6
  81. package/dist/runner/changeCapture.js +287 -81
  82. package/dist/runner/cli.js +7 -6
  83. package/dist/runner/postTaskCleanup.js +60 -7
  84. package/dist/runner/processSecurity.d.ts +56 -0
  85. package/dist/runner/processSecurity.js +373 -0
  86. package/dist/runner/runTask.d.ts +1 -1
  87. package/dist/runner/runTask.js +339 -287
  88. package/dist/runner/simpleProcess.d.ts +3 -0
  89. package/dist/runner/simpleProcess.js +52 -38
  90. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  91. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  92. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  93. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  94. package/dist/runner/taskStatusStore.d.ts +11 -0
  95. package/dist/runner/taskStatusStore.js +24 -0
  96. package/dist/runner/watch.d.ts +14 -1
  97. package/dist/runner/watch.js +265 -54
  98. package/dist/security/contentRedaction.d.ts +6 -0
  99. package/dist/security/contentRedaction.js +24 -2
  100. package/dist/security/discoveryTokenStore.d.ts +3 -2
  101. package/dist/security/discoveryTokenStore.js +17 -2
  102. package/dist/security/loopbackHost.d.ts +2 -0
  103. package/dist/security/loopbackHost.js +16 -0
  104. package/dist/security/pathGuard.js +32 -17
  105. package/dist/security/planGuard.js +2 -1
  106. package/dist/security/runtimeGuard.js +9 -6
  107. package/dist/security/secretComparison.d.ts +3 -0
  108. package/dist/security/secretComparison.js +11 -0
  109. package/dist/security/sensitiveGuard.d.ts +5 -0
  110. package/dist/security/sensitiveGuard.js +27 -8
  111. package/dist/security/toolInvocationGuard.d.ts +2 -2
  112. package/dist/security/toolInvocationGuard.js +1 -1
  113. package/dist/security/workspaceRootGuard.d.ts +8 -0
  114. package/dist/security/workspaceRootGuard.js +47 -0
  115. package/dist/smoke-test.js +280 -273
  116. package/dist/tools/catalog/index.d.ts +4 -0
  117. package/dist/tools/catalog/index.js +4 -0
  118. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  119. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  120. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
  121. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  122. package/dist/tools/definitions/toolDefs.js +1177 -0
  123. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  124. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  125. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
  126. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
  127. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  128. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
  129. package/dist/tools/diagnostics/index.d.ts +6 -0
  130. package/dist/tools/diagnostics/index.js +6 -0
  131. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  132. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  133. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
  134. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
  135. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  136. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  137. package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
  138. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
  139. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
  140. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  141. package/dist/tools/direct/index.d.ts +3 -0
  142. package/dist/tools/direct/index.js +3 -0
  143. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  144. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  145. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  146. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  147. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  148. package/dist/tools/discovery/index.d.ts +3 -0
  149. package/dist/tools/discovery/index.js +3 -0
  150. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  151. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  152. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  153. package/dist/tools/dispatch/coreDispatch.js +283 -0
  154. package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
  155. package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
  156. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  157. package/dist/tools/dispatch/directDispatch.js +116 -0
  158. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  159. package/dist/tools/dispatch/goalDispatch.js +92 -0
  160. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  161. package/dist/tools/dispatch/releaseDispatch.js +46 -0
  162. package/dist/tools/dispatch/types.d.ts +23 -0
  163. package/dist/tools/dispatch/types.js +15 -0
  164. package/dist/tools/dispatch/validation.d.ts +7 -0
  165. package/dist/tools/dispatch/validation.js +71 -0
  166. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  167. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
  168. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  169. package/dist/tools/goals/index.d.ts +3 -0
  170. package/dist/tools/goals/index.js +3 -0
  171. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  172. package/dist/tools/registry.d.ts +10 -14
  173. package/dist/tools/registry.js +47 -1651
  174. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  175. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  176. package/dist/tools/release/index.d.ts +2 -0
  177. package/dist/tools/release/index.js +2 -0
  178. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  179. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  180. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  181. package/dist/tools/tasks/cancelTask.js +85 -0
  182. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  183. package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
  184. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  185. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  186. package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
  187. package/dist/tools/tasks/evidencePack.js +387 -0
  188. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  189. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  190. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  191. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  192. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  193. package/dist/tools/tasks/index.d.ts +18 -0
  194. package/dist/tools/tasks/index.js +18 -0
  195. package/dist/tools/tasks/killTask.d.ts +1 -0
  196. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  197. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  198. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  199. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  200. package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
  201. package/dist/tools/tasks/retryTask.js +60 -0
  202. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  203. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
  204. package/dist/tools/tasks/runVerification.js +38 -0
  205. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  206. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  207. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  208. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  209. package/dist/tools/workspace/applyPatch.js +53 -0
  210. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  211. package/dist/tools/workspace/index.d.ts +10 -0
  212. package/dist/tools/workspace/index.js +10 -0
  213. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  214. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  215. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  216. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  217. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  218. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  219. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  220. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  221. package/dist/tools/workspace/syncFile.js +134 -0
  222. package/dist/utils/atomicFile.d.ts +7 -0
  223. package/dist/utils/atomicFile.js +48 -0
  224. package/dist/utils/boundedFile.d.ts +8 -0
  225. package/dist/utils/boundedFile.js +93 -0
  226. package/dist/utils/lockedJsonFile.d.ts +15 -0
  227. package/dist/utils/lockedJsonFile.js +224 -0
  228. package/dist/utils/platform.d.ts +11 -0
  229. package/dist/utils/platform.js +26 -0
  230. package/dist/utils/stableJson.d.ts +2 -0
  231. package/dist/utils/stableJson.js +12 -0
  232. package/dist/version.d.ts +3 -2
  233. package/dist/version.js +3 -2
  234. package/dist/watcherStatus.d.ts +2 -2
  235. package/dist/watcherStatus.js +18 -0
  236. package/docs/CODE_WIKI.md +1648 -0
  237. package/docs/agentseal-integration.md +150 -0
  238. package/docs/architecture.md +23 -0
  239. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  240. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  241. package/docs/control-center/README.md +4 -4
  242. package/docs/dashboard-overview.md +86 -0
  243. package/docs/demo.md +8 -0
  244. package/docs/desktop-app.md +175 -0
  245. package/docs/direct-session-workflow.md +98 -0
  246. package/docs/evidence-pack-schema.md +215 -0
  247. package/docs/lineage-evidence-pack-workflow.md +127 -0
  248. package/docs/mcp-inspector-testing.md +200 -0
  249. package/docs/open-source-application.md +168 -0
  250. package/docs/opencode-worker.md +151 -0
  251. package/docs/openhands-worker.md +181 -0
  252. package/docs/release-checklist.md +1 -1
  253. package/docs/release-evidence.md +82 -0
  254. package/docs/spec-kit-integration.md +131 -0
  255. package/docs/task-safe-review-workflow.md +98 -0
  256. package/docs/threat-model.md +97 -0
  257. package/docs/user-feedback.md +40 -0
  258. package/docs/why-patchwarden.md +110 -0
  259. package/examples/config.example.json +4 -2
  260. package/package.json +28 -11
  261. package/scripts/README.md +15 -6
  262. package/scripts/build.js +11 -0
  263. package/scripts/checks/brand-check.js +3 -0
  264. package/scripts/checks/build-output-check.js +37 -0
  265. package/scripts/checks/control-center-smoke.js +447 -6
  266. package/scripts/checks/control-smoke.js +19 -8
  267. package/scripts/checks/http-mcp-smoke.js +82 -4
  268. package/scripts/checks/lifecycle-smoke.js +39 -33
  269. package/scripts/checks/mcp-manifest-check.js +1 -1
  270. package/scripts/checks/mcp-smoke.js +30 -4
  271. package/scripts/checks/package-manifest-check.js +20 -7
  272. package/scripts/checks/unit-tests.js +3 -5
  273. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  274. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  275. package/scripts/control/manage-patchwarden.ps1 +69 -4
  276. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  277. package/scripts/control/run-background-supervisor.ps1 +41 -0
  278. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  279. package/scripts/generate-demo-gif.py +320 -0
  280. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  281. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  282. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  283. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  284. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  285. package/scripts/lib/clean-generated-output.js +11 -0
  286. package/scripts/lib/file-discovery.js +12 -0
  287. package/scripts/release/desktop-preflight.js +211 -0
  288. package/scripts/release/pack-clean.js +59 -14
  289. package/src/assessments/agentAssessor.ts +29 -15
  290. package/src/assessments/assessmentStore.ts +55 -43
  291. package/src/assessments/confirmCli.ts +5 -4
  292. package/src/config.ts +105 -11
  293. package/src/control/fileManager.ts +40 -0
  294. package/src/control/middleware/auth.ts +35 -0
  295. package/src/control/middleware/static.ts +84 -0
  296. package/src/control/routeTable.ts +301 -0
  297. package/src/control/routes/audit.ts +329 -0
  298. package/src/control/routes/evidence.ts +107 -0
  299. package/src/control/routes/lineage.ts +92 -0
  300. package/src/control/routes/policy.ts +81 -0
  301. package/src/control/routes/process.ts +505 -0
  302. package/src/control/routes/sessions.ts +314 -0
  303. package/src/control/routes/status.ts +405 -0
  304. package/src/control/routes/taskActions.ts +214 -0
  305. package/src/control/routes/tasks.ts +331 -0
  306. package/src/control/routes/workspace.ts +234 -0
  307. package/src/control/runtime.ts +508 -0
  308. package/src/control/server.ts +165 -0
  309. package/src/control/shared.ts +360 -0
  310. package/src/controlCenter.ts +7 -2347
  311. package/src/direct/directAudit.ts +17 -9
  312. package/src/direct/directGuards.ts +100 -51
  313. package/src/direct/directPatch.ts +95 -8
  314. package/src/direct/directSessionStore.ts +211 -55
  315. package/src/direct/directVerification.ts +9 -23
  316. package/src/doctor.ts +766 -492
  317. package/src/goal/acceptanceEngine.ts +1 -1
  318. package/src/goal/goalProgress.ts +17 -13
  319. package/src/goal/goalReport.ts +263 -0
  320. package/src/goal/goalStatus.ts +25 -3
  321. package/src/goal/goalStore.ts +241 -54
  322. package/src/goal/handoffExport.ts +6 -6
  323. package/src/goal/specKitImport.ts +355 -0
  324. package/src/goal/subgoalSync.ts +15 -10
  325. package/src/goal/worktreeManager.ts +139 -22
  326. package/src/httpServer.ts +130 -44
  327. package/src/index.ts +7 -4
  328. package/src/logging.ts +20 -4
  329. package/src/policy/projectPolicy.ts +5 -2
  330. package/src/release/releaseGate.ts +74 -27
  331. package/src/runner/agentInvocation.ts +126 -4
  332. package/src/runner/changeCapture.ts +342 -80
  333. package/src/runner/cli.ts +7 -6
  334. package/src/runner/postTaskCleanup.ts +55 -7
  335. package/src/runner/processSecurity.ts +433 -0
  336. package/src/runner/runTask.ts +441 -292
  337. package/src/runner/simpleProcess.ts +68 -33
  338. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  339. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  340. package/src/runner/taskStatusStore.ts +54 -0
  341. package/src/runner/watch.ts +290 -57
  342. package/src/security/contentRedaction.ts +31 -2
  343. package/src/security/discoveryTokenStore.ts +18 -4
  344. package/src/security/loopbackHost.ts +19 -0
  345. package/src/security/pathGuard.ts +35 -16
  346. package/src/security/planGuard.ts +2 -1
  347. package/src/security/runtimeGuard.ts +9 -6
  348. package/src/security/secretComparison.ts +13 -0
  349. package/src/security/sensitiveGuard.ts +27 -8
  350. package/src/security/toolInvocationGuard.ts +2 -2
  351. package/src/security/workspaceRootGuard.ts +53 -0
  352. package/src/smoke-test.ts +276 -273
  353. package/src/tools/catalog/index.ts +4 -0
  354. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  355. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
  356. package/src/tools/definitions/toolDefs.ts +1264 -0
  357. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  358. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
  359. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
  360. package/src/tools/diagnostics/index.ts +6 -0
  361. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  362. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
  363. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  364. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
  365. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
  366. package/src/tools/direct/index.ts +3 -0
  367. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  368. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  369. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  370. package/src/tools/discovery/index.ts +3 -0
  371. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  372. package/src/tools/dispatch/coreDispatch.ts +379 -0
  373. package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
  374. package/src/tools/dispatch/directDispatch.ts +168 -0
  375. package/src/tools/dispatch/goalDispatch.ts +128 -0
  376. package/src/tools/dispatch/releaseDispatch.ts +66 -0
  377. package/src/tools/dispatch/types.ts +24 -0
  378. package/src/tools/dispatch/validation.ts +78 -0
  379. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  380. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
  381. package/src/tools/goals/index.ts +3 -0
  382. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  383. package/src/tools/registry.ts +54 -1858
  384. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  385. package/src/tools/release/index.ts +2 -0
  386. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  387. package/src/tools/tasks/cancelTask.ts +109 -0
  388. package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
  389. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  390. package/src/tools/tasks/evidencePack.ts +504 -0
  391. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  392. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  393. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  394. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  395. package/src/tools/tasks/index.ts +18 -0
  396. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  397. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  398. package/src/tools/tasks/retryTask.ts +69 -0
  399. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
  400. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  401. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  402. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  403. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  404. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  405. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  406. package/src/tools/workspace/index.ts +10 -0
  407. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  408. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  409. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  410. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  411. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  412. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  413. package/src/tools/workspace/syncFile.ts +292 -0
  414. package/src/utils/atomicFile.ts +58 -0
  415. package/src/utils/boundedFile.ts +116 -0
  416. package/src/utils/lockedJsonFile.ts +251 -0
  417. package/src/utils/platform.ts +31 -0
  418. package/src/utils/stableJson.ts +12 -0
  419. package/src/version.ts +3 -2
  420. package/src/watcherStatus.ts +19 -1
  421. package/ui/desktop-bootstrap.js +11 -0
  422. package/ui/desktop-bridge.js +36 -0
  423. package/ui/desktop.css +160 -0
  424. package/ui/getting-started.js +93 -0
  425. package/ui/i18n.js +484 -0
  426. package/ui/log-parser.js +30 -0
  427. package/ui/pages/audit.html +204 -3
  428. package/ui/pages/dashboard.html +937 -79
  429. package/ui/pages/direct-sessions.html +609 -60
  430. package/ui/pages/getting-started.html +43 -0
  431. package/ui/pages/logs.html +105 -22
  432. package/ui/pages/settings.html +101 -0
  433. package/ui/pages/task-detail.html +468 -438
  434. package/ui/pages/tasks.html +648 -65
  435. package/ui/pages/workspace.html +38 -1
  436. package/ui/settings.js +245 -0
  437. package/PatchWarden-Control.cmd +0 -6
  438. package/Restart-PatchWarden-Control.cmd +0 -6
  439. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  440. package/dist/test/unit/acceptance-engine.test.js +0 -228
  441. package/dist/test/unit/agent-router.test.d.ts +0 -1
  442. package/dist/test/unit/agent-router.test.js +0 -287
  443. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  444. package/dist/test/unit/android-doctor.test.js +0 -118
  445. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  446. package/dist/test/unit/audit-checks.test.js +0 -350
  447. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  448. package/dist/test/unit/chinese-path.test.js +0 -91
  449. package/dist/test/unit/command-guard.test.d.ts +0 -1
  450. package/dist/test/unit/command-guard.test.js +0 -160
  451. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  452. package/dist/test/unit/diagnose-task.test.js +0 -457
  453. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  454. package/dist/test/unit/direct-guards.test.js +0 -213
  455. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  456. package/dist/test/unit/discovery-token-store.test.js +0 -139
  457. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  458. package/dist/test/unit/evidence-pack.test.js +0 -130
  459. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  460. package/dist/test/unit/goal-graph.test.js +0 -298
  461. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  462. package/dist/test/unit/goal-progress.test.js +0 -381
  463. package/dist/test/unit/goal-status.test.d.ts +0 -1
  464. package/dist/test/unit/goal-status.test.js +0 -215
  465. package/dist/test/unit/goal-store.test.d.ts +0 -1
  466. package/dist/test/unit/goal-store.test.js +0 -253
  467. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  468. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  469. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  470. package/dist/test/unit/goal-tools-registry.test.js +0 -190
  471. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  472. package/dist/test/unit/handoff-export.test.js +0 -263
  473. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  474. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  475. package/dist/test/unit/logging.test.d.ts +0 -1
  476. package/dist/test/unit/logging.test.js +0 -397
  477. package/dist/test/unit/path-guard.test.d.ts +0 -1
  478. package/dist/test/unit/path-guard.test.js +0 -109
  479. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  480. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  481. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  482. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  483. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  484. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  485. package/dist/test/unit/release-gate.test.d.ts +0 -1
  486. package/dist/test/unit/release-gate.test.js +0 -242
  487. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  488. package/dist/test/unit/run-task-loop.test.js +0 -380
  489. package/dist/test/unit/safe-status.test.d.ts +0 -1
  490. package/dist/test/unit/safe-status.test.js +0 -165
  491. package/dist/test/unit/safe-views.test.d.ts +0 -1
  492. package/dist/test/unit/safe-views.test.js +0 -171
  493. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  494. package/dist/test/unit/schema-drift-check.test.js +0 -176
  495. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  496. package/dist/test/unit/sensitive-guard.test.js +0 -104
  497. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  498. package/dist/test/unit/subgoal-sync.test.js +0 -183
  499. package/dist/test/unit/sync-file.test.d.ts +0 -1
  500. package/dist/test/unit/sync-file.test.js +0 -154
  501. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  502. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  503. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  504. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  505. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  506. package/dist/test/unit/toolSearch.test.js +0 -571
  507. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  508. package/dist/test/unit/watcher-status.test.js +0 -169
  509. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  510. package/dist/test/unit/worktree-manager.test.js +0 -176
  511. package/dist/tools/applyPatch.js +0 -41
  512. package/dist/tools/cancelTask.d.ts +0 -30
  513. package/dist/tools/cancelTask.js +0 -64
  514. package/dist/tools/evidencePack.js +0 -168
  515. package/dist/tools/finalizeDirectSession.js +0 -84
  516. package/dist/tools/goalSubgoalTask.js +0 -110
  517. package/dist/tools/killTask.d.ts +0 -15
  518. package/dist/tools/retryTask.js +0 -45
  519. package/dist/tools/runVerification.js +0 -36
  520. package/dist/tools/syncFile.js +0 -65
  521. package/docs/control-center/control-center-daily-driver.md +0 -211
  522. package/docs/control-center/control-center-mvp.md +0 -205
  523. package/docs/control-center/control-center-phase2.md +0 -159
  524. package/docs/release-v0.6.0.md +0 -71
  525. package/docs/release-v0.6.1.md +0 -75
  526. package/docs/release-v0.6.4.md +0 -45
  527. package/src/test/unit/acceptance-engine.test.ts +0 -261
  528. package/src/test/unit/agent-router.test.ts +0 -342
  529. package/src/test/unit/android-doctor.test.ts +0 -158
  530. package/src/test/unit/audit-checks.test.ts +0 -567
  531. package/src/test/unit/chinese-path.test.ts +0 -106
  532. package/src/test/unit/command-guard.test.ts +0 -221
  533. package/src/test/unit/diagnose-task.test.ts +0 -544
  534. package/src/test/unit/direct-guards.test.ts +0 -297
  535. package/src/test/unit/discovery-token-store.test.ts +0 -181
  536. package/src/test/unit/evidence-pack.test.ts +0 -142
  537. package/src/test/unit/goal-graph.test.ts +0 -347
  538. package/src/test/unit/goal-progress.test.ts +0 -538
  539. package/src/test/unit/goal-status.test.ts +0 -270
  540. package/src/test/unit/goal-store.test.ts +0 -318
  541. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  542. package/src/test/unit/goal-tools-registry.test.ts +0 -243
  543. package/src/test/unit/handoff-export.test.ts +0 -295
  544. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  545. package/src/test/unit/logging.test.ts +0 -497
  546. package/src/test/unit/path-guard.test.ts +0 -150
  547. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  548. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  549. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  550. package/src/test/unit/release-gate.test.ts +0 -314
  551. package/src/test/unit/run-task-loop.test.ts +0 -425
  552. package/src/test/unit/safe-status.test.ts +0 -187
  553. package/src/test/unit/safe-views.test.ts +0 -184
  554. package/src/test/unit/schema-drift-check.test.ts +0 -260
  555. package/src/test/unit/sensitive-guard.test.ts +0 -124
  556. package/src/test/unit/subgoal-sync.test.ts +0 -236
  557. package/src/test/unit/sync-file.test.ts +0 -231
  558. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  559. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  560. package/src/test/unit/toolSearch.test.ts +0 -756
  561. package/src/test/unit/watcher-status.test.ts +0 -190
  562. package/src/test/unit/worktree-manager.test.ts +0 -247
  563. package/src/tools/cancelTask.ts +0 -70
  564. package/src/tools/evidencePack.ts +0 -205
  565. package/src/tools/retryTask.ts +0 -53
  566. package/src/tools/syncFile.ts +0 -122
  567. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  568. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  569. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  570. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  571. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  572. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  573. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  574. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  575. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  576. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  577. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  578. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  579. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  580. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  581. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  582. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  583. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  584. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  585. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  586. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  587. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  588. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  589. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  590. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  591. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  592. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  593. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  594. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  595. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  596. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  597. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -1,6 +1,46 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
3
3
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { platform } from "node:os";
5
+ import { buildGitEnvironment, resolveTrustedExecutable } from "./processSecurity.js";
6
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
7
+
8
+ const isWindows = platform() === "win32";
9
+
10
+ /**
11
+ * Remove a file or directory, working around Node.js fs.rmSync issues on Windows
12
+ * where recursive directory deletion silently fails (Node v22+).
13
+ * On Windows, clears read-only attributes before one bounded native retry.
14
+ */
15
+ function safeRemoveSync(targetPath: string): void {
16
+ rmSync(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
17
+ if (isWindows && existsSync(targetPath)) {
18
+ makeWritableSync(targetPath);
19
+ rmSync(targetPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 150 });
20
+ }
21
+ if (existsSync(targetPath)) {
22
+ throw new Error(`Cleanup path still exists after bounded retries: ${targetPath}`);
23
+ }
24
+ }
25
+
26
+ function makeWritableSync(targetPath: string): void {
27
+ let stat: ReturnType<typeof lstatSync>;
28
+ try {
29
+ stat = lstatSync(targetPath);
30
+ } catch {
31
+ return;
32
+ }
33
+ if (stat.isSymbolicLink()) return;
34
+ try { chmodSync(targetPath, stat.isDirectory() ? 0o700 : 0o600); } catch { /* best effort */ }
35
+ if (!stat.isDirectory()) return;
36
+ let entries: string[];
37
+ try {
38
+ entries = readdirSync(targetPath);
39
+ } catch {
40
+ return;
41
+ }
42
+ for (const entry of entries) makeWritableSync(join(targetPath, entry));
43
+ }
4
44
 
5
45
  const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
6
46
  const CLEANUP_DIRS = new Set(["__pycache__", "dist", "release_packages", "temp_files"]);
@@ -42,7 +82,7 @@ export function runPostTaskCleanup(repoPath: string, taskDir: string): PostTaskC
42
82
  continue;
43
83
  }
44
84
  try {
45
- rmSync(candidate.path, { recursive: true, force: true });
85
+ safeRemoveSync(candidate.path);
46
86
  report.removed.push({ path: rel, reason: candidate.reason });
47
87
  } catch (error) {
48
88
  report.skipped.push({
@@ -52,7 +92,7 @@ export function runPostTaskCleanup(repoPath: string, taskDir: string): PostTaskC
52
92
  });
53
93
  }
54
94
  }
55
- writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(report, null, 2), "utf-8");
95
+ atomicWriteJsonFileSync(join(taskDir, "post-task-cleanup.json"), report);
56
96
  return report;
57
97
  }
58
98
 
@@ -93,10 +133,12 @@ function walk(dir: string, visit: (path: string, isDir: boolean) => void): void
93
133
  function hasTrackedGitContent(root: string, rel: string): boolean {
94
134
  if (!isGitWorktree(root)) return false;
95
135
  try {
96
- const output = execFileSync("git", ["ls-files", "--", rel], {
136
+ const env = buildGitEnvironment(root);
137
+ const output = execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["ls-files", "--", rel], {
97
138
  cwd: root,
98
139
  encoding: "utf-8",
99
140
  windowsHide: true,
141
+ env,
100
142
  });
101
143
  return output.trim().length > 0;
102
144
  } catch {
@@ -106,19 +148,23 @@ function hasTrackedGitContent(root: string, rel: string): boolean {
106
148
 
107
149
  function isIgnoredOrUntracked(root: string, rel: string): boolean {
108
150
  if (!isGitWorktree(root)) return true;
151
+ const env = buildGitEnvironment(root);
152
+ const git = resolveTrustedExecutable("git", root, { pathValue: env.PATH });
109
153
  try {
110
- execFileSync("git", ["check-ignore", "-q", "--", rel], {
154
+ execFileSync(git, ["check-ignore", "-q", "--", rel], {
111
155
  cwd: root,
112
156
  stdio: "ignore",
113
157
  windowsHide: true,
158
+ env,
114
159
  });
115
160
  return true;
116
161
  } catch {
117
162
  try {
118
- const output = execFileSync("git", ["ls-files", "--others", "--exclude-standard", "--", rel], {
163
+ const output = execFileSync(git, ["ls-files", "--others", "--exclude-standard", "--", rel], {
119
164
  cwd: root,
120
165
  encoding: "utf-8",
121
166
  windowsHide: true,
167
+ env,
122
168
  });
123
169
  return output.trim().length > 0 || !existsSync(resolve(root, rel));
124
170
  } catch {
@@ -129,10 +175,12 @@ function isIgnoredOrUntracked(root: string, rel: string): boolean {
129
175
 
130
176
  function isGitWorktree(root: string): boolean {
131
177
  try {
132
- return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
178
+ const env = buildGitEnvironment(root);
179
+ return execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["rev-parse", "--is-inside-work-tree"], {
133
180
  cwd: root,
134
181
  encoding: "utf-8",
135
182
  windowsHide: true,
183
+ env,
136
184
  }).trim() === "true";
137
185
  } catch {
138
186
  return false;
@@ -0,0 +1,433 @@
1
+ import { appendFileSync, existsSync, statSync } from "node:fs";
2
+ import { delimiter, isAbsolute, relative, resolve, sep, win32 } from "node:path";
3
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
4
+
5
+ const WINDOWS_BASE_ENVIRONMENT = [
6
+ "SystemRoot",
7
+ "WINDIR",
8
+ "ComSpec",
9
+ "PATHEXT",
10
+ "PATH",
11
+ "TEMP",
12
+ "TMP",
13
+ "USERPROFILE",
14
+ "HOMEDRIVE",
15
+ "HOMEPATH",
16
+ "APPDATA",
17
+ "LOCALAPPDATA",
18
+ "PROGRAMDATA",
19
+ "ProgramFiles",
20
+ "ProgramFiles(x86)",
21
+ "ProgramW6432",
22
+ "HOME",
23
+ "XDG_CONFIG_HOME",
24
+ "XDG_CACHE_HOME",
25
+ "XDG_DATA_HOME",
26
+ "JAVA_HOME",
27
+ "ANDROID_HOME",
28
+ "ANDROID_SDK_ROOT",
29
+ "CARGO_HOME",
30
+ "RUSTUP_HOME",
31
+ "GOPATH",
32
+ "GOROOT",
33
+ "PYTHONHOME",
34
+ "VIRTUAL_ENV",
35
+ ] as const;
36
+
37
+ const POSIX_BASE_ENVIRONMENT = [
38
+ "PATH",
39
+ "HOME",
40
+ "TMPDIR",
41
+ "USER",
42
+ "LOGNAME",
43
+ "SHELL",
44
+ "LANG",
45
+ "LC_ALL",
46
+ "LC_CTYPE",
47
+ "TERM",
48
+ "COLORTERM",
49
+ "NO_COLOR",
50
+ "FORCE_COLOR",
51
+ "XDG_CONFIG_HOME",
52
+ "XDG_CACHE_HOME",
53
+ "XDG_DATA_HOME",
54
+ ] as const;
55
+
56
+ const ALWAYS_BLOCKED_ENVIRONMENT = new Set([
57
+ "CONTROL_PLANE_API_KEY",
58
+ "PATCHWARDEN_OWNER_TOKEN",
59
+ ]);
60
+
61
+ const WINDOWS_EXECUTABLE_EXTENSIONS = [".COM", ".EXE", ".BAT", ".CMD"];
62
+ const LOG_TRUNCATION_MARKER = "\n[PATCHWARDEN LOG TRUNCATED]\n";
63
+ export const DEFAULT_MAX_PROCESS_LOG_BYTES = 2 * 1024 * 1024;
64
+
65
+ export interface ChildEnvironmentOptions {
66
+ cwd: string;
67
+ allowedNames?: readonly string[];
68
+ blockedNames?: readonly string[];
69
+ sourceEnvironment?: NodeJS.ProcessEnv;
70
+ platform?: NodeJS.Platform;
71
+ }
72
+
73
+ export interface ExecutableResolutionOptions {
74
+ platform?: NodeJS.Platform;
75
+ pathValue?: string;
76
+ pathExtValue?: string;
77
+ fileExists?: (path: string) => boolean;
78
+ }
79
+
80
+ export interface ResolvedProcessCommand {
81
+ command: string;
82
+ argsPrefix: string[];
83
+ }
84
+
85
+ export interface PreparedProcessCommand {
86
+ command: string;
87
+ args: string[];
88
+ }
89
+
90
+ /**
91
+ * Build the environment for a task-owned child. Provider credentials are not
92
+ * ambient capability: an agent receives them only when their variable names
93
+ * are explicitly allow-listed in trusted local configuration.
94
+ */
95
+ export function buildChildEnvironment(options: ChildEnvironmentOptions): NodeJS.ProcessEnv {
96
+ const source = options.sourceEnvironment ?? process.env;
97
+ const platform = options.platform ?? process.platform;
98
+ const blocked = new Set([
99
+ ...ALWAYS_BLOCKED_ENVIRONMENT,
100
+ ...(options.blockedNames ?? []).map((name) => name.toUpperCase()),
101
+ ]);
102
+ const requested = [
103
+ ...(platform === "win32" ? WINDOWS_BASE_ENVIRONMENT : POSIX_BASE_ENVIRONMENT),
104
+ ...(options.allowedNames ?? []),
105
+ ];
106
+ const environment: NodeJS.ProcessEnv = {};
107
+
108
+ for (const name of requested) {
109
+ if (!isEnvironmentVariableName(name)) {
110
+ throw new Error(`Invalid child environment variable name: "${name}"`);
111
+ }
112
+ if (blocked.has(name.toUpperCase())) continue;
113
+ const entry = findEnvironmentEntry(source, name, platform);
114
+ if (entry && entry[1] !== undefined) environment[entry[0]] = entry[1];
115
+ }
116
+
117
+ if (platform === "win32") {
118
+ // The MCP SDK's safe default environment includes SystemRoot and PATH but
119
+ // intentionally omits ComSpec and PATHEXT. npm 10 can start in that
120
+ // environment, but its lifecycle shell then exits before running even a
121
+ // simple Node command. Reconstruct only the standard Windows process
122
+ // plumbing from the already inherited OS root; never copy extra ambient
123
+ // variables or task-controlled values.
124
+ const systemRoot = findEnvironmentEntry(environment, "SystemRoot", platform)?.[1]
125
+ ?? findEnvironmentEntry(environment, "WINDIR", platform)?.[1];
126
+ if (!findEnvironmentEntry(environment, "ComSpec", platform) && systemRoot && win32.isAbsolute(systemRoot)) {
127
+ environment.ComSpec = win32.join(systemRoot, "System32", "cmd.exe");
128
+ }
129
+ if (!findEnvironmentEntry(environment, "PATHEXT", platform)) {
130
+ environment.PATHEXT = ".COM;.EXE;.BAT;.CMD";
131
+ }
132
+ }
133
+
134
+ const pathEntry = findEnvironmentEntry(environment, "PATH", platform);
135
+ if (pathEntry) {
136
+ environment[pathEntry[0]] = sanitizeTrustedPath(pathEntry[1] || "", options.cwd, platform);
137
+ }
138
+ return environment;
139
+ }
140
+
141
+ /** Build a non-interactive Git environment that disables repo-defined hooks and fsmonitor. */
142
+ export function buildGitEnvironment(cwd: string): NodeJS.ProcessEnv {
143
+ const environment = buildChildEnvironment({ cwd });
144
+ environment.GIT_OPTIONAL_LOCKS = "0";
145
+ environment.GIT_TERMINAL_PROMPT = "0";
146
+ environment.GCM_INTERACTIVE = "Never";
147
+ environment.GIT_CONFIG_COUNT = "2";
148
+ environment.GIT_CONFIG_KEY_0 = "core.fsmonitor";
149
+ environment.GIT_CONFIG_VALUE_0 = "false";
150
+ environment.GIT_CONFIG_KEY_1 = "core.hooksPath";
151
+ environment.GIT_CONFIG_VALUE_1 = "/dev/null";
152
+ return environment;
153
+ }
154
+
155
+ /** Resolve a Windows bare command before spawn so CreateProcess never searches cwd. */
156
+ export function resolveTrustedExecutable(
157
+ command: string,
158
+ cwd: string,
159
+ options: ExecutableResolutionOptions = {},
160
+ ): string {
161
+ const platform = options.platform ?? process.platform;
162
+ if (platform !== "win32" || win32.isAbsolute(command)) return command;
163
+ if (!command || /[\\/]/.test(command)) {
164
+ throw new Error(`Executable must be an absolute path or bare command name: "${command}"`);
165
+ }
166
+
167
+ const trustedPath = sanitizeTrustedPath(
168
+ options.pathValue ?? process.env.PATH ?? "",
169
+ cwd,
170
+ platform,
171
+ );
172
+ const roots = trustedPath.split(win32.delimiter).filter(Boolean);
173
+ const extension = win32.extname(command);
174
+ const configuredExtensions = (options.pathExtValue ?? process.env.PATHEXT ?? "")
175
+ .split(win32.delimiter)
176
+ .map((value) => value.trim().toUpperCase())
177
+ .filter((value) => WINDOWS_EXECUTABLE_EXTENSIONS.includes(value));
178
+ const extensions = extension
179
+ ? [""]
180
+ : [...new Set([...configuredExtensions, ...WINDOWS_EXECUTABLE_EXTENSIONS])];
181
+ const fileExists = options.fileExists ?? existsSync;
182
+
183
+ for (const root of roots) {
184
+ for (const suffix of extensions) {
185
+ const candidate = win32.resolve(root, `${command}${suffix}`);
186
+ if (isPathInside(candidate, cwd, "win32")) continue;
187
+ if (fileExists(candidate)) return candidate;
188
+ }
189
+ }
190
+ throw new Error(`Executable not found in trusted PATH: "${command}"`);
191
+ }
192
+
193
+ /**
194
+ * Resolve Windows package-manager shims to their JavaScript CLI entry point.
195
+ * This keeps npm/npx/pnpm execution shell-free while still supporting their
196
+ * normal .cmd installations.
197
+ */
198
+ export function resolvePackageManagerInvocation(
199
+ managerCommand: string,
200
+ cwd: string,
201
+ options: ExecutableResolutionOptions = {},
202
+ ): ResolvedProcessCommand {
203
+ const platform = options.platform ?? process.platform;
204
+ if (platform !== "win32") return { command: managerCommand, argsPrefix: [] };
205
+ const manager = win32.basename(managerCommand).replace(/\.(?:cmd|bat|exe|com)$/i, "").toLowerCase();
206
+ if (!new Set(["npm", "npx", "pnpm"]).has(manager)) {
207
+ throw new Error(`Unsupported package manager command: "${managerCommand}"`);
208
+ }
209
+ const resolved = resolveTrustedExecutable(managerCommand, cwd, options);
210
+ if (/\.(?:exe|com)$/i.test(resolved)) return { command: resolved, argsPrefix: [] };
211
+
212
+ const fileExists = options.fileExists ?? existsSync;
213
+ const roots: string[] = [];
214
+ let current = win32.dirname(resolved);
215
+ for (let depth = 0; depth < 5; depth++) {
216
+ roots.push(current);
217
+ const parent = win32.dirname(current);
218
+ if (parent === current) break;
219
+ current = parent;
220
+ }
221
+ const relativeCliPaths = manager === "npm"
222
+ ? ["node_modules\\npm\\bin\\npm-cli.js"]
223
+ : manager === "npx"
224
+ ? ["node_modules\\npm\\bin\\npx-cli.js"]
225
+ : [
226
+ "node_modules\\pnpm\\bin\\pnpm.cjs",
227
+ "node_modules\\pnpm\\bin\\pnpm.mjs",
228
+ "node\\node_modules\\pnpm\\bin\\pnpm.cjs",
229
+ "node\\node_modules\\pnpm\\bin\\pnpm.mjs",
230
+ ];
231
+ for (const root of roots) {
232
+ for (const relativeCli of relativeCliPaths) {
233
+ const cliPath = win32.join(root, relativeCli);
234
+ if (fileExists(cliPath)) return { command: process.execPath, argsPrefix: [cliPath] };
235
+ }
236
+ }
237
+ throw new Error(`Native CLI entry point not found for trusted package-manager shim: "${resolved}"`);
238
+ }
239
+
240
+ /** Replace the one supported Windows cmd wrapper with a native Node CLI. */
241
+ export function prepareShellFreeCommand(
242
+ command: string,
243
+ args: readonly string[],
244
+ cwd: string,
245
+ options: ExecutableResolutionOptions = {},
246
+ ): PreparedProcessCommand {
247
+ const platform = options.platform ?? process.platform;
248
+ if (platform !== "win32") {
249
+ return { command, args: [...args] };
250
+ }
251
+ // Node cannot reliably execute a .cmd shim with shell:false on Windows.
252
+ // Resolve direct package-manager invocations to their JavaScript CLI too,
253
+ // so callers do not need to manufacture a cmd.exe /c wrapper.
254
+ if (/^(?:npm|npx|pnpm)(?:\.cmd)?$/i.test(win32.basename(command))) {
255
+ const resolved = resolvePackageManagerInvocation(command, cwd, options);
256
+ return { command: resolved.command, args: [...resolved.argsPrefix, ...args] };
257
+ }
258
+ if (!/^cmd(?:\.exe)?$/i.test(win32.basename(command))) {
259
+ return { command, args: [...args] };
260
+ }
261
+ const commandIndex = args.findIndex((arg) => /^\/c$/i.test(arg));
262
+ const prefix = commandIndex >= 0 ? args.slice(0, commandIndex) : [];
263
+ const manager = commandIndex >= 0 ? args[commandIndex + 1] : undefined;
264
+ if (
265
+ commandIndex < 0
266
+ || prefix.some((arg) => !/^\/(?:d|s)$/i.test(arg))
267
+ || !manager
268
+ || !/^(?:npm|npx|pnpm)(?:\.cmd)?$/i.test(manager)
269
+ ) {
270
+ throw new Error("Windows command shells are not allowed for task-owned child processes");
271
+ }
272
+ const resolved = resolvePackageManagerInvocation(manager, cwd, options);
273
+ return {
274
+ command: resolved.command,
275
+ args: [...resolved.argsPrefix, ...args.slice(commandIndex + 2)],
276
+ };
277
+ }
278
+
279
+ /** Parse the intentionally simple allow-listed command grammar without a shell. */
280
+ export function resolveTrustedCommandLine(
281
+ commandLine: string,
282
+ cwd: string,
283
+ options: ExecutableResolutionOptions = {},
284
+ ): ResolvedProcessCommand {
285
+ const parts = commandLine.trim().split(/\s+/).filter(Boolean);
286
+ if (parts.length === 0) throw new Error("Command must not be empty");
287
+ if (parts.some((part) => /[\x00-\x1f"'|&;<>()`$]/.test(part))) {
288
+ throw new Error(`Command contains unsupported shell syntax: "${commandLine}"`);
289
+ }
290
+ if (/^(?:npm|npx|pnpm)(?:\.cmd)?$/i.test(parts[0])) {
291
+ const manager = resolvePackageManagerInvocation(parts[0], cwd, options);
292
+ return { command: manager.command, argsPrefix: [...manager.argsPrefix, ...parts.slice(1)] };
293
+ }
294
+ return {
295
+ command: resolveTrustedExecutable(parts[0], cwd, options),
296
+ argsPrefix: parts.slice(1),
297
+ };
298
+ }
299
+
300
+ export function sanitizeTrustedPath(
301
+ pathValue: string,
302
+ cwd: string,
303
+ platform: NodeJS.Platform = process.platform,
304
+ ): string {
305
+ const pathApi = platform === "win32" ? win32 : { delimiter, isAbsolute, relative, resolve, sep };
306
+ const seen = new Set<string>();
307
+ const entries: string[] = [];
308
+ for (const rawEntry of pathValue.split(pathApi.delimiter)) {
309
+ const unquoted = rawEntry.trim().replace(/^"|"$/g, "");
310
+ if (!unquoted || !pathApi.isAbsolute(unquoted)) continue;
311
+ const absolute = pathApi.resolve(unquoted);
312
+ if (isPathInside(absolute, cwd, platform)) continue;
313
+ const comparable = platform === "win32" ? absolute.toLowerCase() : absolute;
314
+ if (seen.has(comparable)) continue;
315
+ seen.add(comparable);
316
+ entries.push(absolute);
317
+ }
318
+ return entries.join(pathApi.delimiter);
319
+ }
320
+
321
+ export function allowedEnvironmentValues(
322
+ names: readonly string[] | undefined,
323
+ sourceEnvironment: NodeJS.ProcessEnv = process.env,
324
+ platform: NodeJS.Platform = process.platform,
325
+ ): string[] {
326
+ const values = new Set<string>();
327
+ for (const name of names ?? []) {
328
+ if (ALWAYS_BLOCKED_ENVIRONMENT.has(name.toUpperCase())) continue;
329
+ const value = findEnvironmentEntry(sourceEnvironment, name, platform)?.[1];
330
+ if (value && Buffer.byteLength(value, "utf-8") >= 8) values.add(value);
331
+ }
332
+ return [...values].sort((left, right) => right.length - left.length);
333
+ }
334
+
335
+ export function redactProcessOutput(input: string, exactValues: readonly string[] = []): string {
336
+ let content = redactSensitiveContent(input).content;
337
+ for (const value of exactValues) {
338
+ if (value) content = content.split(value).join("[REDACTED ENV VALUE]");
339
+ }
340
+ return content;
341
+ }
342
+
343
+ /** Captures complete process logs up to one auditable, redacted byte budget. */
344
+ export class SecureProcessLogCapture {
345
+ private readonly chunks = new Map<string, Buffer[]>();
346
+ private readonly maxBytes: number;
347
+ private readonly existingBytes: number;
348
+ private capturedBytes = 0;
349
+ private truncatedPath: string | null = null;
350
+
351
+ constructor(paths: Array<string | undefined>, maxBytes = DEFAULT_MAX_PROCESS_LOG_BYTES) {
352
+ if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
353
+ throw new Error("maxLogBytes must be a positive integer");
354
+ }
355
+ this.maxBytes = maxBytes;
356
+ const uniquePaths = [...new Set(paths.filter((path): path is string => Boolean(path)))];
357
+ this.existingBytes = uniquePaths.reduce((total, path) => total + fileSize(path), 0);
358
+ }
359
+
360
+ append(path: string | undefined, chunk: Buffer | string): void {
361
+ if (!path) return;
362
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
363
+ const remaining = Math.max(0, this.maxBytes - this.existingBytes - this.capturedBytes);
364
+ const accepted = data.subarray(0, remaining);
365
+ if (accepted.length > 0) {
366
+ const chunks = this.chunks.get(path) ?? [];
367
+ chunks.push(accepted);
368
+ this.chunks.set(path, chunks);
369
+ this.capturedBytes += accepted.length;
370
+ }
371
+ if (accepted.length < data.length) this.truncatedPath = path;
372
+ }
373
+
374
+ flush(exactValues: readonly string[] = []): void {
375
+ if (this.chunks.size === 0 && !this.truncatedPath) return;
376
+ const remaining = Math.max(0, this.maxBytes - this.existingBytes);
377
+ const entries = [...this.chunks.entries()];
378
+ if (this.truncatedPath && !this.chunks.has(this.truncatedPath)) {
379
+ entries.push([this.truncatedPath, []]);
380
+ }
381
+
382
+ const markerBudget = this.truncatedPath
383
+ ? Math.min(remaining, Buffer.byteLength(LOG_TRUNCATION_MARKER, "utf-8"))
384
+ : 0;
385
+ let contentRemaining = remaining - markerBudget;
386
+ for (const [path, chunks] of entries) {
387
+ const redacted = redactProcessOutput(Buffer.concat(chunks).toString("utf-8"), exactValues);
388
+ const content = utf8Prefix(redacted, contentRemaining);
389
+ contentRemaining -= Buffer.byteLength(content, "utf-8");
390
+ const marker = path === this.truncatedPath
391
+ ? utf8Prefix(LOG_TRUNCATION_MARKER, markerBudget)
392
+ : "";
393
+ const output = content + marker;
394
+ if (output) {
395
+ try { appendFileSync(path, output, "utf-8"); } catch { /* evidence logging is best effort */ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ function findEnvironmentEntry(
402
+ environment: NodeJS.ProcessEnv,
403
+ name: string,
404
+ platform: NodeJS.Platform,
405
+ ): [string, string | undefined] | undefined {
406
+ if (platform !== "win32") {
407
+ return Object.prototype.hasOwnProperty.call(environment, name) ? [name, environment[name]] : undefined;
408
+ }
409
+ const match = Object.keys(environment).find((key) => key.toUpperCase() === name.toUpperCase());
410
+ return match ? [match, environment[match]] : undefined;
411
+ }
412
+
413
+ function isEnvironmentVariableName(name: string): boolean {
414
+ return typeof name === "string" && name.length > 0 && !/[=\x00]/.test(name);
415
+ }
416
+
417
+ function isPathInside(candidate: string, parent: string, platform: NodeJS.Platform): boolean {
418
+ const pathApi = platform === "win32" ? win32 : { relative, isAbsolute, sep };
419
+ const rel = pathApi.relative(parent, candidate);
420
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(rel));
421
+ }
422
+
423
+ function fileSize(path: string): number {
424
+ try { return statSync(path).size; } catch { return 0; }
425
+ }
426
+
427
+ function utf8Prefix(value: string, maxBytes: number): string {
428
+ const encoded = Buffer.from(value, "utf-8");
429
+ if (encoded.length <= maxBytes) return value;
430
+ let prefix = encoded.subarray(0, maxBytes).toString("utf-8").replace(/\uFFFD$/u, "");
431
+ while (Buffer.byteLength(prefix, "utf-8") > maxBytes) prefix = prefix.slice(0, -1);
432
+ return prefix;
433
+ }