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,8 +1,9 @@
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 { buildAgentInvocation, buildAssessmentPrompt } from "../runner/agentInvocation.js";
4
3
  import { runSimpleProcessSync } from "../runner/simpleProcess.js";
5
4
  import { captureRepoSnapshot, compareSnapshots } from "../runner/changeCapture.js";
5
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
6
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
6
7
  const ASSESSMENT_MARKER = "===ASSESSMENT_JSON===";
7
8
  export async function runAgentAssessment(input) {
8
9
  const logPaths = {
@@ -26,9 +27,11 @@ export async function runAgentAssessment(input) {
26
27
  log_paths: logPaths,
27
28
  };
28
29
  // ── 1. Build prompt and write to file for {prompt_file} support ──
29
- const assessmentPrompt = buildAssessmentPrompt(input.goal, input.planContent, input.repoPath);
30
+ const safeGoal = redactSensitiveContent(input.goal).content.slice(0, 10_000);
31
+ const safePlan = redactSensitiveContent(input.planContent).content.slice(0, 1024 * 1024);
32
+ const assessmentPrompt = buildAssessmentPrompt(safeGoal, safePlan, input.repoPath);
30
33
  const promptFilePath = resolve(input.assessmentDir, "agent-assessment-prompt.md");
31
- writeFileSync(promptFilePath, assessmentPrompt, "utf-8");
34
+ atomicWriteFileSync(promptFilePath, assessmentPrompt);
32
35
  logPaths.prompt = promptFilePath;
33
36
  // ── 2. Before snapshot (repo-scoped) ──
34
37
  let repoBefore;
@@ -68,6 +71,8 @@ export async function runAgentAssessment(input) {
68
71
  maxStderrBytes: Math.max(16384, Math.floor(input.maxOutputBytes / 4)),
69
72
  stdoutPath: logPaths.stdout,
70
73
  stderrPath: logPaths.stderr,
74
+ environmentVariableNames: invocation.environmentVariableNames,
75
+ blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
71
76
  });
72
77
  // ── 5. After snapshot + read-only violation check ──
73
78
  let readOnlyViolation = false;
@@ -101,11 +106,11 @@ export async function runAgentAssessment(input) {
101
106
  stderr_truncated: result.stderrTruncated,
102
107
  log_paths: { ...logPaths, violation: resolve(input.assessmentDir, "agent-assessment-violation.json") },
103
108
  };
104
- writeFileSync(summary.log_paths.violation, JSON.stringify({
109
+ atomicWriteJsonFileSync(summary.log_paths.violation, {
105
110
  violation: "read_only_violation",
106
111
  files: violationFiles,
107
112
  exit_code: result.exitCode,
108
- }, null, 2), "utf-8");
113
+ });
109
114
  writeSummary(logPaths.assessment, summary);
110
115
  return summary;
111
116
  }
@@ -216,13 +221,17 @@ function parseAssessmentJson(stdout, repoPath) {
216
221
  const jsonEnd = jsonText.lastIndexOf("}");
217
222
  if (jsonStart < 0 || jsonEnd < 0 || jsonEnd <= jsonStart)
218
223
  return { output: null, sanitized_reasons };
219
- let parsed;
224
+ let parsedValue;
220
225
  try {
221
- parsed = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
226
+ parsedValue = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
222
227
  }
223
228
  catch {
224
229
  return { output: null, sanitized_reasons };
225
230
  }
231
+ if (!parsedValue || typeof parsedValue !== "object" || Array.isArray(parsedValue)) {
232
+ return { output: null, sanitized_reasons };
233
+ }
234
+ const parsed = parsedValue;
226
235
  // ── Strict validation ──
227
236
  if (parsed.risk_level !== "low" && parsed.risk_level !== "medium" && parsed.risk_level !== "high") {
228
237
  return { output: null, sanitized_reasons };
@@ -235,7 +244,7 @@ function parseAssessmentJson(stdout, repoPath) {
235
244
  return { output: null, sanitized_reasons };
236
245
  if (parsed.reason_codes.length > 50)
237
246
  return { output: null, sanitized_reasons };
238
- const reasonCodes = parsed.reason_codes.filter((r) => typeof r === "string" && r.length <= 100);
247
+ const reasonCodes = parsed.reason_codes.filter((reason) => typeof reason === "string" && reason.length <= 100);
239
248
  if (reasonCodes.length !== parsed.reason_codes.length) {
240
249
  sanitized_reasons.push("reason_codes_filtered");
241
250
  }
@@ -257,7 +266,7 @@ function parseAssessmentJson(stdout, repoPath) {
257
266
  }
258
267
  const resolved = resolve(repoPath, p);
259
268
  const rel = relative(repoPath, resolved);
260
- if (rel.startsWith("..")) {
269
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
261
270
  pathsSanitized = true;
262
271
  continue;
263
272
  }
@@ -270,7 +279,7 @@ function parseAssessmentJson(stdout, repoPath) {
270
279
  return { output: null, sanitized_reasons };
271
280
  if (parsed.destructive_actions.length > 20)
272
281
  return { output: null, sanitized_reasons };
273
- const destructiveActions = parsed.destructive_actions.filter((d) => typeof d === "string" && d.length <= 200);
282
+ const destructiveActions = parsed.destructive_actions.filter((action) => typeof action === "string" && action.length <= 200);
274
283
  if (typeof parsed.requires_user_confirm !== "boolean")
275
284
  return { output: null, sanitized_reasons };
276
285
  const notes = typeof parsed.notes === "string" ? parsed.notes.slice(0, 2000) : "";
@@ -289,5 +298,5 @@ function parseAssessmentJson(stdout, repoPath) {
289
298
  };
290
299
  }
291
300
  function writeSummary(path, summary) {
292
- writeFileSync(path, JSON.stringify(summary, null, 2), "utf-8");
301
+ atomicWriteJsonFileSync(path, summary);
293
302
  }
@@ -1,11 +1,14 @@
1
- import { mkdirSync, writeFileSync, existsSync, readFileSync, renameSync, } from "node:fs";
1
+ import { mkdirSync, existsSync, readFileSync, } from "node:fs";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { resolve, join } from "node:path";
4
4
  import { getAssessmentsDir, getConfig, getRepoAllowedTestCommands } from "../config.js";
5
5
  import { guardPath } from "../security/pathGuard.js";
6
6
  import { PatchWardenError } from "../errors.js";
7
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
8
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
9
+ import { withFileLockSync } from "../utils/lockedJsonFile.js";
7
10
  import { TOOL_SCHEMA_EPOCH } from "../version.js";
8
- import { getLastToolCatalogSnapshot } from "../tools/toolCatalog.js";
11
+ import { getLastToolCatalogSnapshot } from "../tools/catalog/toolCatalog.js";
9
12
  import { captureRepoSnapshot } from "../runner/changeCapture.js";
10
13
  const SENSITIVE_PATH_RULES_SIGNATURE = "v1";
11
14
  const ARTIFACT_RULES_SIGNATURE = "v1";
@@ -71,7 +74,7 @@ export function createAssessment(input) {
71
74
  repo_path: input.repo_path,
72
75
  resolved_repo_path: input.resolved_repo_path,
73
76
  template: input.template || null,
74
- goal: input.goal || null,
77
+ goal: input.goal ? redactSensitiveContent(input.goal).content : null,
75
78
  test_command: input.test_command || null,
76
79
  verify_commands: input.verify_commands || [],
77
80
  agent: input.agent,
@@ -83,7 +86,7 @@ export function createAssessment(input) {
83
86
  confirm_code: null,
84
87
  agent_assessment_summary: input.agent_assessment_summary || null,
85
88
  };
86
- writeFileSync(join(dir, "assessment.json"), JSON.stringify(record, null, 2), "utf-8");
89
+ atomicWriteJsonFileSync(join(dir, "assessment.json"), record);
87
90
  return record;
88
91
  }
89
92
  export function readAssessment(assessmentId) {
@@ -160,6 +163,9 @@ export async function confirmAssessment(assessmentId) {
160
163
  if (!/^assessment_\d{8}_\d{6}_[0-9a-f]{32}$/.test(assessmentId)) {
161
164
  throw new PatchWardenError("assessment_id_invalid", "Local confirmation requires the full assessment_id (32 hexadecimal random characters).", "Copy the complete assessment_id from the assess_only response; assessment_short_id is display-only.");
162
165
  }
166
+ const config = getConfig();
167
+ const assessmentFile = resolve(getAssessmentsDir(config), assessmentId, "assessment.json");
168
+ guardPath(assessmentFile, config.workspaceRoot, config.assessmentsDir);
163
169
  const assessment = readAssessment(assessmentId);
164
170
  if (assessment.decision !== "needs_confirm" || !assessment.requires_confirm) {
165
171
  throw new PatchWardenError("assessment_confirmation_not_allowed", `Assessment "${assessmentId}" has decision "${assessment.decision}" and cannot be locally confirmed.`, assessment.decision === "blocked"
@@ -167,37 +173,39 @@ export async function confirmAssessment(assessmentId) {
167
173
  : "This assessment does not require confirmation; execute it with the minimal assessment_id call.");
168
174
  }
169
175
  const snapshot = await captureRepoSnapshot(assessment.resolved_repo_path);
170
- const validation = validateAssessmentFreshness(assessmentId, snapshot, {
171
- allow_unconfirmed: true,
172
- // A standalone CLI has no active MCP profile snapshot. The execute path
173
- // still performs the full tool-manifest check before creating the task.
174
- skip_tool_manifest: true,
176
+ // Re-read and write while holding one cross-process lock. Concurrent local
177
+ // confirmers may compute snapshots in parallel, but they cannot replace one
178
+ // another's confirmation metadata from a stale assessment record.
179
+ return withFileLockSync(assessmentFile, () => {
180
+ const validation = validateAssessmentFreshness(assessmentId, snapshot, {
181
+ allow_unconfirmed: true,
182
+ // A standalone CLI has no active MCP profile snapshot. The execute path
183
+ // still performs the full tool-manifest check before creating the task.
184
+ skip_tool_manifest: true,
185
+ });
186
+ if (!validation.valid || !validation.assessment) {
187
+ throw new PatchWardenError(validation.failure_reason || "assessment_validation_failed", `Assessment "${assessmentId}" cannot be confirmed: ${validation.failure_reason || "validation_failed"}.`, "Run create_task with execution_mode=assess_only again, then confirm the new assessment locally.");
188
+ }
189
+ if (validation.assessment.decision !== "needs_confirm" || !validation.assessment.requires_confirm) {
190
+ throw new PatchWardenError("assessment_confirmation_not_allowed", `Assessment "${assessmentId}" is no longer eligible for local confirmation.`, "Run create_task with execution_mode=assess_only again.");
191
+ }
192
+ const confirmedAt = validation.assessment.confirmed_at || new Date().toISOString();
193
+ const confirmedRecord = {
194
+ ...validation.assessment,
195
+ confirmed: true,
196
+ confirmed_at: confirmedAt,
197
+ confirm_code: validation.assessment.confirm_code || randomBytes(8).toString("hex"),
198
+ };
199
+ atomicWriteJsonFileSync(assessmentFile, confirmedRecord);
200
+ return {
201
+ assessment_id: assessmentId,
202
+ decision: "needs_confirm",
203
+ confirmed: true,
204
+ confirmed_at: confirmedAt,
205
+ expires_at: confirmedRecord.expires_at,
206
+ next_action: "Call create_task with only execution_mode=execute and this full assessment_id.",
207
+ };
175
208
  });
176
- if (!validation.valid || !validation.assessment) {
177
- throw new PatchWardenError(validation.failure_reason || "assessment_validation_failed", `Assessment "${assessmentId}" cannot be confirmed: ${validation.failure_reason || "validation_failed"}.`, "Run create_task with execution_mode=assess_only again, then confirm the new assessment locally.");
178
- }
179
- const confirmedAt = validation.assessment.confirmed_at || new Date().toISOString();
180
- const confirmedRecord = {
181
- ...validation.assessment,
182
- confirmed: true,
183
- confirmed_at: confirmedAt,
184
- confirm_code: validation.assessment.confirm_code || randomBytes(8).toString("hex"),
185
- };
186
- const config = getConfig();
187
- const assessmentFile = resolve(getAssessmentsDir(config), assessmentId, "assessment.json");
188
- guardPath(assessmentFile, config.workspaceRoot, config.assessmentsDir);
189
- const temporaryFile = `${assessmentFile}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
190
- guardPath(temporaryFile, config.workspaceRoot, config.assessmentsDir);
191
- writeFileSync(temporaryFile, JSON.stringify(confirmedRecord, null, 2), "utf-8");
192
- renameSync(temporaryFile, assessmentFile);
193
- return {
194
- assessment_id: assessmentId,
195
- decision: "needs_confirm",
196
- confirmed: true,
197
- confirmed_at: confirmedAt,
198
- expires_at: confirmedRecord.expires_at,
199
- next_action: "Call create_task with only execution_mode=execute and this full assessment_id.",
200
- };
201
209
  }
202
210
  export function computeWorkspaceFingerprint(snapshot) {
203
211
  const fileHashes = Object.entries(snapshot.files)
package/dist/config.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export interface AgentConfig {
2
2
  command: string;
3
3
  args: string[];
4
+ envAllowlist?: string[];
5
+ adapter?: string;
6
+ model?: string;
4
7
  }
5
8
  export interface PatchWardenConfig {
6
9
  workspaceRoot: string;
@@ -29,6 +32,18 @@ export interface PatchWardenConfig {
29
32
  agentAssessmentMaxOutputBytes?: number;
30
33
  agentAssessmentAgentName?: string;
31
34
  enableDirectProfile?: boolean;
35
+ tunnelClientPath?: string;
36
+ tunnelProxy?: {
37
+ scope: "shared" | "separate";
38
+ core: {
39
+ mode: "environment" | "none" | "manual";
40
+ url?: string;
41
+ };
42
+ direct: {
43
+ mode: "environment" | "none" | "manual";
44
+ url?: string;
45
+ };
46
+ };
32
47
  directSessionsDir: string;
33
48
  directSessionTtlSeconds: number;
34
49
  directMaxPatchBytes: number;
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
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
  // ── Defaults ──────────────────────────────────────────────────────
4
5
  const DEFAULT_CONFIG = {
5
6
  workspaceRoot: process.cwd(),
@@ -7,16 +8,7 @@ const DEFAULT_CONFIG = {
7
8
  tasksDir: ".patchwarden/tasks",
8
9
  assessmentsDir: ".patchwarden/assessments",
9
10
  assessmentTtlSeconds: 3600,
10
- agents: {
11
- codex: {
12
- command: "codex",
13
- args: ["exec", "--cd", "{repo}", "{prompt}"],
14
- },
15
- opencode: {
16
- command: "opencode",
17
- args: ["run", "{prompt}"],
18
- },
19
- },
11
+ agents: {},
20
12
  allowedTestCommands: [
21
13
  "npm test",
22
14
  "npm run test",
@@ -123,6 +115,11 @@ function normalizeConfig(config) {
123
115
  if (!config.workspaceRoot || typeof config.workspaceRoot !== "string") {
124
116
  throw new Error("workspaceRoot must be a non-empty string");
125
117
  }
118
+ const workspaceValidation = validateWorkspaceRoot(config.workspaceRoot);
119
+ if (!workspaceValidation.ok) {
120
+ throw new Error(`Invalid workspaceRoot: ${workspaceValidation.reason} (${workspaceValidation.path})`);
121
+ }
122
+ config.workspaceRoot = workspaceValidation.path;
126
123
  if (!config.plansDir || typeof config.plansDir !== "string") {
127
124
  throw new Error("plansDir must be a non-empty string");
128
125
  }
@@ -138,6 +135,21 @@ function normalizeConfig(config) {
138
135
  if (!config.agents || typeof config.agents !== "object") {
139
136
  throw new Error("agents must be an object");
140
137
  }
138
+ const agents = {};
139
+ for (const [agentName, agent] of Object.entries(config.agents)) {
140
+ if (!agent || typeof agent !== "object" || typeof agent.command !== "string" || !Array.isArray(agent.args)) {
141
+ throw new Error(`agents["${agentName}"] must define command and args`);
142
+ }
143
+ const envAllowlist = agent.envAllowlist ?? [];
144
+ if (!Array.isArray(envAllowlist) || envAllowlist.some((name) => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) {
145
+ throw new Error(`agents["${agentName}"].envAllowlist must contain only environment variable names`);
146
+ }
147
+ const blockedNames = new Set(["CONTROL_PLANE_API_KEY", "PATCHWARDEN_OWNER_TOKEN", config.http?.ownerTokenEnv?.toUpperCase()]);
148
+ const blocked = envAllowlist.find((name) => blockedNames.has(name.toUpperCase()));
149
+ if (blocked)
150
+ throw new Error(`agents["${agentName}"].envAllowlist cannot include reserved variable "${blocked}"`);
151
+ agents[agentName] = { ...agent, args: [...agent.args], envAllowlist: [...new Set(envAllowlist)] };
152
+ }
141
153
  if (!Array.isArray(config.allowedTestCommands)) {
142
154
  throw new Error("allowedTestCommands must be an array");
143
155
  }
@@ -209,8 +221,10 @@ function normalizeConfig(config) {
209
221
  if (config.http.host !== undefined && typeof config.http.host !== "string") {
210
222
  throw new Error("http.host must be a string");
211
223
  }
212
- if (config.http.ownerTokenEnv !== undefined && typeof config.http.ownerTokenEnv !== "string") {
213
- throw new Error("http.ownerTokenEnv must be a string");
224
+ if (config.http.ownerTokenEnv !== undefined
225
+ && (typeof config.http.ownerTokenEnv !== "string"
226
+ || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.http.ownerTokenEnv))) {
227
+ throw new Error("http.ownerTokenEnv must be a valid environment variable name");
214
228
  }
215
229
  }
216
230
  if (config.enableRunTaskTool !== undefined && typeof config.enableRunTaskTool !== "boolean") {
@@ -235,6 +249,44 @@ function normalizeConfig(config) {
235
249
  if (config.enableDirectProfile !== undefined && typeof config.enableDirectProfile !== "boolean") {
236
250
  throw new Error("enableDirectProfile must be a boolean");
237
251
  }
252
+ if (config.tunnelClientPath !== undefined) {
253
+ if (typeof config.tunnelClientPath !== "string" || !isAbsolute(config.tunnelClientPath)) {
254
+ throw new Error("tunnelClientPath must be an absolute path");
255
+ }
256
+ if (basename(config.tunnelClientPath).toLowerCase() !== "tunnel-client.exe") {
257
+ throw new Error("tunnelClientPath must point to tunnel-client.exe");
258
+ }
259
+ }
260
+ if (config.tunnelProxy !== undefined) {
261
+ if (!config.tunnelProxy || typeof config.tunnelProxy !== "object" || !["shared", "separate"].includes(config.tunnelProxy.scope)) {
262
+ throw new Error("tunnelProxy.scope must be shared or separate");
263
+ }
264
+ for (const key of ["core", "direct"]) {
265
+ const endpoint = config.tunnelProxy[key];
266
+ if (!endpoint || typeof endpoint !== "object" || !["environment", "none", "manual"].includes(endpoint.mode)) {
267
+ throw new Error(`tunnelProxy.${key}.mode must be environment, none, or manual`);
268
+ }
269
+ if (endpoint.mode === "manual") {
270
+ if (typeof endpoint.url !== "string")
271
+ throw new Error(`tunnelProxy.${key}.url is required in manual mode`);
272
+ let parsed;
273
+ try {
274
+ parsed = new URL(endpoint.url);
275
+ }
276
+ catch {
277
+ throw new Error(`tunnelProxy.${key}.url must be a valid URL`);
278
+ }
279
+ if (!["http:", "https:", "socks5:"].includes(parsed.protocol)) {
280
+ throw new Error(`tunnelProxy.${key}.url must use http, https, or socks5`);
281
+ }
282
+ if (parsed.username || parsed.password)
283
+ throw new Error(`tunnelProxy.${key}.url must not contain credentials`);
284
+ }
285
+ else if (endpoint.url !== undefined) {
286
+ throw new Error(`tunnelProxy.${key}.url is only allowed in manual mode`);
287
+ }
288
+ }
289
+ }
238
290
  if (!config.directSessionsDir || typeof config.directSessionsDir !== "string") {
239
291
  throw new Error("directSessionsDir must be a non-empty string");
240
292
  }
@@ -276,6 +328,7 @@ function normalizeConfig(config) {
276
328
  }
277
329
  return {
278
330
  ...config,
331
+ agents,
279
332
  workspaceRoot: resolve(config.workspaceRoot),
280
333
  allowedTestCommands: [...new Set(config.allowedTestCommands.map((command) => command.trim()))],
281
334
  repoAllowedTestCommands,
@@ -0,0 +1,2 @@
1
+ /** Launch a trusted platform file manager without inheriting ambient secrets. */
2
+ export declare function launchFileManager(target: string, cwd: string): boolean;
@@ -0,0 +1,39 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { buildChildEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
4
+ const FILE_MANAGER_ENVIRONMENT = [
5
+ "DISPLAY",
6
+ "WAYLAND_DISPLAY",
7
+ "DBUS_SESSION_BUS_ADDRESS",
8
+ "XDG_RUNTIME_DIR",
9
+ ];
10
+ /** Launch a trusted platform file manager without inheriting ambient secrets. */
11
+ export function launchFileManager(target, cwd) {
12
+ try {
13
+ const env = buildChildEnvironment({ cwd, allowedNames: FILE_MANAGER_ENVIRONMENT });
14
+ const requested = fileManagerCommand(env);
15
+ const command = resolveTrustedExecutable(requested, cwd, { pathValue: env.PATH });
16
+ const child = spawn(command, [target], {
17
+ cwd,
18
+ detached: true,
19
+ env,
20
+ stdio: "ignore",
21
+ windowsHide: true,
22
+ });
23
+ child.on("error", () => { });
24
+ child.unref();
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function fileManagerCommand(env) {
32
+ if (process.platform === "win32") {
33
+ const windowsRoot = env.SystemRoot || env.WINDIR;
34
+ return windowsRoot ? join(windowsRoot, "explorer.exe") : "explorer.exe";
35
+ }
36
+ if (process.platform === "darwin")
37
+ return "open";
38
+ return "xdg-open";
39
+ }
@@ -8,3 +8,9 @@
8
8
  */
9
9
  import { type IncomingMessage } from "node:http";
10
10
  export declare function checkControlToken(req: IncomingMessage): boolean;
11
+ /**
12
+ * Reject non-loopback Host headers before exposing the browser-readable
13
+ * control token. Binding to 127.0.0.1 alone does not prevent DNS rebinding.
14
+ */
15
+ export declare function isTrustedControlHost(req: IncomingMessage, expectedPort: number): boolean;
16
+ export declare function isTrustedControlHostHeader(value: string | string[] | undefined, expectedPort: number): boolean;
@@ -1,8 +1,17 @@
1
+ import { firstHeaderValue, timingSafeStringEqual } from "../../security/secretComparison.js";
2
+ import { isTrustedLoopbackHostHeader } from "../../security/loopbackHost.js";
1
3
  import { controlToken } from "../shared.js";
2
4
  export function checkControlToken(req) {
3
- const header = req.headers["x-patchwarden-control-token"];
4
- const provided = Array.isArray(header) ? header[0] : header;
5
- if (typeof provided !== "string" || provided.length === 0)
6
- return false;
7
- return provided === controlToken;
5
+ const provided = firstHeaderValue(req.headers["x-patchwarden-control-token"]);
6
+ return provided.length > 0 && timingSafeStringEqual(provided, controlToken);
7
+ }
8
+ /**
9
+ * Reject non-loopback Host headers before exposing the browser-readable
10
+ * control token. Binding to 127.0.0.1 alone does not prevent DNS rebinding.
11
+ */
12
+ export function isTrustedControlHost(req, expectedPort) {
13
+ return isTrustedControlHostHeader(req.headers.host, expectedPort);
14
+ }
15
+ export function isTrustedControlHostHeader(value, expectedPort) {
16
+ return isTrustedLoopbackHostHeader(value, expectedPort);
8
17
  }
@@ -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 { CONTENT_TYPES, CONTROL_CENTER_FAVICON, errorMessage, sendJson, uiRoot, } from "../shared.js";
11
11
  export function serveStatic(res, urlPath) {
@@ -45,9 +45,22 @@ export function serveStatic(res, urlPath) {
45
45
  sendJson(res, 404, { error: "Not found" });
46
46
  return;
47
47
  }
48
- const ext = extname(candidate).toLowerCase();
48
+ // Resolve links before reading. Lexical `..` checks alone do not stop a
49
+ // symlink or junction inside `ui/` from pointing outside the bundle.
50
+ const realRoot = realpathSync(uiRoot);
51
+ const realCandidate = realpathSync(candidate);
52
+ const realRelative = relative(realRoot, realCandidate);
53
+ if (realRelative === "" || realRelative.startsWith("..") || isAbsolute(realRelative)) {
54
+ sendJson(res, 403, { error: "Forbidden" });
55
+ return;
56
+ }
57
+ if (!statSync(realCandidate).isFile()) {
58
+ sendJson(res, 404, { error: "Not found" });
59
+ return;
60
+ }
61
+ const ext = extname(realCandidate).toLowerCase();
49
62
  const contentType = CONTENT_TYPES[ext] || "application/octet-stream";
50
- const content = readFileSync(candidate);
63
+ const content = readFileSync(realCandidate);
51
64
  res.writeHead(200, { "Content-Type": contentType });
52
65
  res.end(content);
53
66
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * PatchWarden Control Center — declarative route table.
3
+ *
4
+ * Exports `buildRoutes(parsedUrl)` which returns the full `Route[]` array
5
+ * consumed by `server.ts`'s `handleRequest`. Routes that need query-string
6
+ * parameters (e.g. `/api/tasks`, `/api/logs/:category`) close over `parsedUrl`
7
+ * via the `query` helper, so each request gets its own self-contained table
8
+ * without module-scoped request state. This keeps concurrent requests safe.
9
+ *
10
+ * Ordering rules:
11
+ * - GET routes first, POST routes second.
12
+ * - Within each method, more specific patterns precede generic `:id`
13
+ * patterns (e.g. `/api/tasks/stale` before `/api/tasks/:id`).
14
+ * - Static asset routes (`/`, `/favicon.ico`, CSS/JS, `/pages/`,
15
+ * `/partials/`, `/vendor/`) and `/control-token.json` are NOT in this
16
+ * table — they remain priority branches in `server.ts`.
17
+ */
18
+ import { type Route } from "./shared.js";
19
+ /**
20
+ * Build the per-request `Route[]` table. `parsedUrl` is captured by closures
21
+ * so handlers that need query parameters can read `searchParams` without any
22
+ * module-scoped state.
23
+ */
24
+ export declare function buildRoutes(parsedUrl: URL): Route[];