patchwarden 1.5.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (591) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +158 -26
  3. package/README.md +124 -24
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
  6. package/dist/assessments/assessmentDiagnostics.js +132 -0
  7. package/dist/assessments/assessmentStore.d.ts +39 -0
  8. package/dist/assessments/assessmentStore.js +207 -45
  9. package/dist/assessments/securitySnapshot.d.ts +50 -0
  10. package/dist/assessments/securitySnapshot.js +158 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +66 -13
  13. package/dist/control/fileManager.d.ts +2 -0
  14. package/dist/control/fileManager.js +39 -0
  15. package/dist/control/middleware/auth.d.ts +6 -0
  16. package/dist/control/middleware/auth.js +14 -5
  17. package/dist/control/middleware/static.js +16 -3
  18. package/dist/control/routeTable.d.ts +24 -0
  19. package/dist/control/routeTable.js +263 -0
  20. package/dist/control/routes/audit.js +17 -7
  21. package/dist/control/routes/evidence.js +1 -1
  22. package/dist/control/routes/lineage.js +1 -1
  23. package/dist/control/routes/process.d.ts +7 -0
  24. package/dist/control/routes/process.js +329 -42
  25. package/dist/control/routes/sessions.d.ts +9 -1
  26. package/dist/control/routes/sessions.js +84 -21
  27. package/dist/control/routes/status.d.ts +60 -1
  28. package/dist/control/routes/status.js +94 -15
  29. package/dist/control/routes/taskActions.js +24 -60
  30. package/dist/control/routes/tasks.d.ts +4 -0
  31. package/dist/control/routes/tasks.js +61 -62
  32. package/dist/control/routes/workspace.js +50 -23
  33. package/dist/control/runtime.d.ts +3 -2
  34. package/dist/control/runtime.js +65 -40
  35. package/dist/control/server.js +37 -365
  36. package/dist/control/shared.d.ts +25 -1
  37. package/dist/control/shared.js +64 -11
  38. package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
  39. package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
  40. package/dist/direct/directAudit.js +234 -225
  41. package/dist/direct/directGuards.d.ts +2 -1
  42. package/dist/direct/directGuards.js +42 -29
  43. package/dist/direct/directPatch.d.ts +6 -1
  44. package/dist/direct/directPatch.js +55 -8
  45. package/dist/direct/directSessionStore.d.ts +16 -6
  46. package/dist/direct/directSessionStore.js +128 -35
  47. package/dist/direct/directVerification.js +8 -16
  48. package/dist/doctor.js +65 -57
  49. package/dist/goal/acceptanceEngine.d.ts +1 -1
  50. package/dist/goal/goalProgress.js +75 -69
  51. package/dist/goal/goalReport.js +5 -12
  52. package/dist/goal/goalStatus.d.ts +2 -0
  53. package/dist/goal/goalStatus.js +10 -3
  54. package/dist/goal/goalStore.d.ts +9 -2
  55. package/dist/goal/goalStore.js +152 -45
  56. package/dist/goal/handoffExport.js +6 -6
  57. package/dist/goal/specKitImport.js +97 -96
  58. package/dist/goal/subgoalSync.js +11 -9
  59. package/dist/goal/worktreeManager.d.ts +2 -0
  60. package/dist/goal/worktreeManager.js +209 -136
  61. package/dist/httpServer.js +111 -32
  62. package/dist/logging.js +5 -3
  63. package/dist/policy/projectPolicy.js +5 -2
  64. package/dist/release/releaseGate.d.ts +1 -1
  65. package/dist/release/releaseGate.js +78 -25
  66. package/dist/runner/agentInvocation.d.ts +14 -0
  67. package/dist/runner/agentInvocation.js +96 -4
  68. package/dist/runner/changeCapture.d.ts +13 -5
  69. package/dist/runner/changeCapture.js +243 -61
  70. package/dist/runner/postTaskCleanup.js +48 -19
  71. package/dist/runner/processSecurity.d.ts +56 -0
  72. package/dist/runner/processSecurity.js +373 -0
  73. package/dist/runner/runTask.d.ts +2 -1
  74. package/dist/runner/runTask.js +162 -93
  75. package/dist/runner/simpleProcess.d.ts +3 -0
  76. package/dist/runner/simpleProcess.js +50 -36
  77. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  78. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  79. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +3 -1
  80. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  81. package/dist/runner/taskStatusStore.d.ts +11 -0
  82. package/dist/runner/taskStatusStore.js +35 -0
  83. package/dist/runner/watch.d.ts +14 -1
  84. package/dist/runner/watch.js +320 -49
  85. package/dist/security/contentRedaction.js +2 -2
  86. package/dist/security/discoveryTokenStore.d.ts +3 -2
  87. package/dist/security/discoveryTokenStore.js +17 -2
  88. package/dist/security/loopbackHost.d.ts +2 -0
  89. package/dist/security/loopbackHost.js +16 -0
  90. package/dist/security/pathGuard.js +32 -17
  91. package/dist/security/planGuard.js +2 -1
  92. package/dist/security/runtimeGuard.js +9 -6
  93. package/dist/security/secretComparison.d.ts +3 -0
  94. package/dist/security/secretComparison.js +11 -0
  95. package/dist/security/sensitiveGuard.d.ts +5 -0
  96. package/dist/security/sensitiveGuard.js +27 -8
  97. package/dist/security/toolInvocationGuard.d.ts +2 -2
  98. package/dist/security/toolInvocationGuard.js +1 -1
  99. package/dist/security/workspaceRootGuard.d.ts +8 -0
  100. package/dist/security/workspaceRootGuard.js +47 -0
  101. package/dist/smoke-test.js +27 -26
  102. package/dist/tools/catalog/index.d.ts +4 -0
  103. package/dist/tools/catalog/index.js +4 -0
  104. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  105. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  106. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  107. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  108. package/dist/tools/definitions/toolDefs.js +1177 -0
  109. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  110. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  111. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +2 -1
  112. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
  113. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
  114. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
  115. package/dist/tools/diagnostics/index.d.ts +6 -0
  116. package/dist/tools/diagnostics/index.js +6 -0
  117. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  118. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  119. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  120. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  121. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  122. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  123. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  124. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  125. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  126. package/dist/tools/direct/index.d.ts +3 -0
  127. package/dist/tools/direct/index.js +3 -0
  128. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  129. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  130. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  131. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  132. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  133. package/dist/tools/discovery/index.d.ts +3 -0
  134. package/dist/tools/discovery/index.js +3 -0
  135. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  136. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  137. package/dist/tools/dispatch/coreDispatch.js +32 -31
  138. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  139. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  140. package/dist/tools/dispatch/directDispatch.js +11 -10
  141. package/dist/tools/dispatch/goalDispatch.js +5 -4
  142. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  143. package/dist/tools/dispatch/validation.d.ts +7 -0
  144. package/dist/tools/dispatch/validation.js +71 -0
  145. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  146. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  147. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  148. package/dist/tools/goals/index.d.ts +3 -0
  149. package/dist/tools/goals/index.js +3 -0
  150. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  151. package/dist/tools/registry.d.ts +7 -11
  152. package/dist/tools/registry.js +17 -1178
  153. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  154. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  155. package/dist/tools/release/index.d.ts +2 -0
  156. package/dist/tools/release/index.js +2 -0
  157. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  158. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  159. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  160. package/dist/tools/tasks/cancelTask.js +86 -0
  161. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  162. package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
  163. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  164. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
  165. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  166. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  167. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  168. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +11 -6
  169. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  170. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
  171. package/dist/tools/tasks/index.d.ts +18 -0
  172. package/dist/tools/tasks/index.js +18 -0
  173. package/dist/tools/tasks/killTask.d.ts +1 -0
  174. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  175. package/dist/tools/{listTasks.js → tasks/listTasks.js} +5 -4
  176. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
  177. package/dist/tools/tasks/reconcileTasks.js +496 -0
  178. package/dist/tools/tasks/retryTask.js +60 -0
  179. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  180. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +63 -41
  181. package/dist/tools/tasks/runVerification.js +38 -0
  182. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  183. package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
  184. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  185. package/dist/tools/tasks/taskStates.d.ts +6 -0
  186. package/dist/tools/tasks/taskStates.js +52 -0
  187. package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
  188. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  189. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  190. package/dist/tools/workspace/applyPatch.js +53 -0
  191. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  192. package/dist/tools/workspace/index.d.ts +10 -0
  193. package/dist/tools/workspace/index.js +10 -0
  194. package/dist/tools/workspace/listAgents.d.ts +23 -0
  195. package/dist/tools/workspace/listAgents.js +56 -0
  196. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  197. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  198. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  199. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  200. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  201. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  202. package/dist/tools/workspace/syncFile.js +134 -0
  203. package/dist/utils/atomicFile.d.ts +7 -0
  204. package/dist/utils/atomicFile.js +67 -0
  205. package/dist/utils/boundedFile.d.ts +8 -0
  206. package/dist/utils/boundedFile.js +93 -0
  207. package/dist/utils/lockedJsonFile.d.ts +15 -0
  208. package/dist/utils/lockedJsonFile.js +224 -0
  209. package/dist/utils/platform.d.ts +11 -0
  210. package/dist/utils/platform.js +26 -0
  211. package/dist/utils/stableJson.d.ts +2 -0
  212. package/dist/utils/stableJson.js +12 -0
  213. package/dist/version.d.ts +3 -2
  214. package/dist/version.js +3 -2
  215. package/dist/watcherStatus.d.ts +2 -2
  216. package/dist/watcherStatus.js +18 -0
  217. package/docs/CODE_WIKI.md +808 -143
  218. package/docs/architecture.md +15 -55
  219. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  220. package/docs/control-center/README.md +4 -4
  221. package/docs/dashboard-overview.md +4 -4
  222. package/docs/desktop-app.md +175 -0
  223. package/docs/evidence-pack-schema.md +3 -3
  224. package/docs/open-source-application.md +16 -11
  225. package/docs/release-checklist.md +1 -1
  226. package/docs/release-evidence.md +35 -39
  227. package/docs/task-safe-review-workflow.md +5 -5
  228. package/docs/threat-model.md +19 -1
  229. package/examples/config.example.json +4 -2
  230. package/package.json +27 -10
  231. package/scripts/README.md +15 -6
  232. package/scripts/build.js +11 -0
  233. package/scripts/checks/brand-check.js +3 -0
  234. package/scripts/checks/build-output-check.js +37 -0
  235. package/scripts/checks/control-center-smoke.js +102 -10
  236. package/scripts/checks/control-smoke.js +19 -8
  237. package/scripts/checks/http-mcp-smoke.js +82 -4
  238. package/scripts/checks/lifecycle-smoke.js +15 -12
  239. package/scripts/checks/mcp-manifest-check.js +1 -1
  240. package/scripts/checks/mcp-smoke.js +58 -4
  241. package/scripts/checks/package-manifest-check.js +20 -7
  242. package/scripts/checks/unit-tests.js +3 -5
  243. package/scripts/checks/watcher-supervisor-smoke.js +38 -1
  244. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  245. package/scripts/control/manage-patchwarden.ps1 +69 -4
  246. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  247. package/scripts/control/run-background-supervisor.ps1 +41 -0
  248. package/scripts/control/start-patchwarden-tunnel.ps1 +191 -33
  249. package/scripts/e2e/demo-runtime.mjs +153 -0
  250. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  251. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  252. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  253. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  254. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  255. package/scripts/lib/clean-generated-output.js +11 -0
  256. package/scripts/lib/file-discovery.js +12 -0
  257. package/scripts/release/desktop-preflight.js +211 -0
  258. package/scripts/release/pack-clean.js +59 -14
  259. package/src/assessments/agentAssessor.ts +26 -12
  260. package/src/assessments/assessmentDiagnostics.ts +172 -0
  261. package/src/assessments/assessmentStore.ts +288 -51
  262. package/src/assessments/securitySnapshot.ts +231 -0
  263. package/src/config.ts +70 -13
  264. package/src/control/fileManager.ts +40 -0
  265. package/src/control/middleware/auth.ts +22 -4
  266. package/src/control/middleware/static.ts +16 -3
  267. package/src/control/routeTable.ts +301 -0
  268. package/src/control/routes/audit.ts +14 -6
  269. package/src/control/routes/evidence.ts +1 -1
  270. package/src/control/routes/lineage.ts +1 -1
  271. package/src/control/routes/process.ts +341 -40
  272. package/src/control/routes/sessions.ts +88 -25
  273. package/src/control/routes/status.ts +134 -17
  274. package/src/control/routes/taskActions.ts +24 -58
  275. package/src/control/routes/tasks.ts +66 -58
  276. package/src/control/routes/workspace.ts +56 -25
  277. package/src/control/runtime.ts +71 -43
  278. package/src/control/server.ts +37 -342
  279. package/src/control/shared.ts +77 -11
  280. package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
  281. package/src/direct/directAudit.ts +17 -9
  282. package/src/direct/directGuards.ts +72 -44
  283. package/src/direct/directPatch.ts +95 -8
  284. package/src/direct/directSessionStore.ts +213 -55
  285. package/src/direct/directVerification.ts +9 -23
  286. package/src/doctor.ts +76 -64
  287. package/src/goal/acceptanceEngine.ts +1 -1
  288. package/src/goal/goalProgress.ts +17 -13
  289. package/src/goal/goalReport.ts +5 -13
  290. package/src/goal/goalStatus.ts +19 -3
  291. package/src/goal/goalStore.ts +241 -54
  292. package/src/goal/handoffExport.ts +6 -6
  293. package/src/goal/specKitImport.ts +12 -12
  294. package/src/goal/subgoalSync.ts +11 -8
  295. package/src/goal/worktreeManager.ts +139 -22
  296. package/src/httpServer.ts +113 -30
  297. package/src/logging.ts +10 -3
  298. package/src/policy/projectPolicy.ts +5 -2
  299. package/src/release/releaseGate.ts +74 -27
  300. package/src/runner/agentInvocation.ts +126 -4
  301. package/src/runner/changeCapture.ts +291 -57
  302. package/src/runner/postTaskCleanup.ts +41 -17
  303. package/src/runner/processSecurity.ts +433 -0
  304. package/src/runner/runTask.ts +197 -97
  305. package/src/runner/simpleProcess.ts +66 -31
  306. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  307. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
  308. package/src/runner/taskStatusStore.ts +69 -0
  309. package/src/runner/watch.ts +348 -52
  310. package/src/security/contentRedaction.ts +2 -2
  311. package/src/security/discoveryTokenStore.ts +18 -4
  312. package/src/security/loopbackHost.ts +19 -0
  313. package/src/security/pathGuard.ts +35 -16
  314. package/src/security/planGuard.ts +2 -1
  315. package/src/security/runtimeGuard.ts +9 -6
  316. package/src/security/secretComparison.ts +13 -0
  317. package/src/security/sensitiveGuard.ts +27 -8
  318. package/src/security/toolInvocationGuard.ts +2 -2
  319. package/src/security/workspaceRootGuard.ts +53 -0
  320. package/src/smoke-test.ts +27 -26
  321. package/src/tools/catalog/index.ts +4 -0
  322. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  323. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  324. package/src/tools/definitions/toolDefs.ts +1264 -0
  325. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  326. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +136 -39
  327. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
  328. package/src/tools/diagnostics/index.ts +6 -0
  329. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  330. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  331. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  332. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  333. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  334. package/src/tools/direct/index.ts +3 -0
  335. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  336. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  337. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  338. package/src/tools/discovery/index.ts +3 -0
  339. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  340. package/src/tools/dispatch/coreDispatch.ts +36 -31
  341. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  342. package/src/tools/dispatch/directDispatch.ts +11 -10
  343. package/src/tools/dispatch/goalDispatch.ts +5 -4
  344. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  345. package/src/tools/dispatch/validation.ts +78 -0
  346. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  347. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  348. package/src/tools/goals/index.ts +3 -0
  349. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  350. package/src/tools/registry.ts +17 -1261
  351. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  352. package/src/tools/release/index.ts +2 -0
  353. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  354. package/src/tools/tasks/cancelTask.ts +110 -0
  355. package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
  356. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
  357. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  358. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  359. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +13 -6
  360. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  361. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
  362. package/src/tools/tasks/index.ts +18 -0
  363. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
  364. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
  365. package/src/tools/tasks/retryTask.ts +69 -0
  366. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
  367. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  368. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  369. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  370. package/src/tools/tasks/taskStates.ts +54 -0
  371. package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
  372. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  373. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  374. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  375. package/src/tools/workspace/index.ts +10 -0
  376. package/src/tools/workspace/listAgents.ts +75 -0
  377. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  378. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  379. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  380. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  381. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  382. package/src/tools/workspace/syncFile.ts +292 -0
  383. package/src/utils/atomicFile.ts +74 -0
  384. package/src/utils/boundedFile.ts +116 -0
  385. package/src/utils/lockedJsonFile.ts +251 -0
  386. package/src/utils/platform.ts +31 -0
  387. package/src/utils/stableJson.ts +12 -0
  388. package/src/version.ts +3 -2
  389. package/src/watcherStatus.ts +19 -1
  390. package/ui/desktop-bootstrap.js +11 -0
  391. package/ui/desktop-bridge.js +36 -0
  392. package/ui/desktop.css +160 -0
  393. package/ui/getting-started.js +93 -0
  394. package/ui/i18n.js +484 -0
  395. package/ui/log-parser.js +30 -0
  396. package/ui/pages/audit.html +12 -0
  397. package/ui/pages/dashboard.html +94 -147
  398. package/ui/pages/direct-sessions.html +123 -26
  399. package/ui/pages/getting-started.html +43 -0
  400. package/ui/pages/logs.html +134 -22
  401. package/ui/pages/settings.html +101 -0
  402. package/ui/pages/task-detail.html +12 -0
  403. package/ui/pages/tasks.html +54 -8
  404. package/ui/pages/workspace.html +38 -1
  405. package/ui/settings.js +254 -0
  406. package/PatchWarden-Control.cmd +0 -6
  407. package/Restart-PatchWarden-Control.cmd +0 -6
  408. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  409. package/dist/test/unit/acceptance-engine.test.js +0 -228
  410. package/dist/test/unit/agent-router.test.d.ts +0 -1
  411. package/dist/test/unit/agent-router.test.js +0 -287
  412. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  413. package/dist/test/unit/android-doctor.test.js +0 -118
  414. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  415. package/dist/test/unit/apply-patch.test.js +0 -225
  416. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  417. package/dist/test/unit/audit-checks.test.js +0 -350
  418. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  419. package/dist/test/unit/chinese-path.test.js +0 -91
  420. package/dist/test/unit/command-guard.test.d.ts +0 -1
  421. package/dist/test/unit/command-guard.test.js +0 -160
  422. package/dist/test/unit/create-task.test.d.ts +0 -1
  423. package/dist/test/unit/create-task.test.js +0 -197
  424. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  425. package/dist/test/unit/diagnose-task.test.js +0 -457
  426. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  427. package/dist/test/unit/direct-guards.test.js +0 -328
  428. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  429. package/dist/test/unit/discovery-token-store.test.js +0 -139
  430. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  431. package/dist/test/unit/evidence-pack.test.js +0 -224
  432. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  433. package/dist/test/unit/get-task-status.test.js +0 -174
  434. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  435. package/dist/test/unit/get-task-summary.test.js +0 -146
  436. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  437. package/dist/test/unit/goal-graph.test.js +0 -298
  438. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  439. package/dist/test/unit/goal-progress.test.js +0 -381
  440. package/dist/test/unit/goal-report.test.d.ts +0 -1
  441. package/dist/test/unit/goal-report.test.js +0 -159
  442. package/dist/test/unit/goal-status.test.d.ts +0 -1
  443. package/dist/test/unit/goal-status.test.js +0 -215
  444. package/dist/test/unit/goal-store.test.d.ts +0 -1
  445. package/dist/test/unit/goal-store.test.js +0 -253
  446. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  447. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  448. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  449. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  450. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  451. package/dist/test/unit/handoff-export.test.js +0 -263
  452. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  453. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  454. package/dist/test/unit/logging.test.d.ts +0 -1
  455. package/dist/test/unit/logging.test.js +0 -397
  456. package/dist/test/unit/path-guard.test.d.ts +0 -1
  457. package/dist/test/unit/path-guard.test.js +0 -133
  458. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  459. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  460. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  461. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  462. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  463. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  464. package/dist/test/unit/release-gate.test.d.ts +0 -1
  465. package/dist/test/unit/release-gate.test.js +0 -242
  466. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  467. package/dist/test/unit/run-task-loop.test.js +0 -380
  468. package/dist/test/unit/safe-status.test.d.ts +0 -1
  469. package/dist/test/unit/safe-status.test.js +0 -165
  470. package/dist/test/unit/safe-views.test.d.ts +0 -1
  471. package/dist/test/unit/safe-views.test.js +0 -171
  472. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  473. package/dist/test/unit/schema-drift-check.test.js +0 -176
  474. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  475. package/dist/test/unit/sensitive-guard.test.js +0 -104
  476. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  477. package/dist/test/unit/spec-kit-import.test.js +0 -341
  478. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  479. package/dist/test/unit/subgoal-sync.test.js +0 -183
  480. package/dist/test/unit/sync-file.test.d.ts +0 -1
  481. package/dist/test/unit/sync-file.test.js +0 -154
  482. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  483. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  484. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  485. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  486. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  487. package/dist/test/unit/toolSearch.test.js +0 -571
  488. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  489. package/dist/test/unit/wait-for-task.test.js +0 -144
  490. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  491. package/dist/test/unit/watcher-status.test.js +0 -169
  492. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  493. package/dist/test/unit/worktree-manager.test.js +0 -176
  494. package/dist/tools/applyPatch.js +0 -41
  495. package/dist/tools/cancelTask.d.ts +0 -30
  496. package/dist/tools/cancelTask.js +0 -64
  497. package/dist/tools/finalizeDirectSession.js +0 -84
  498. package/dist/tools/goalSubgoalTask.js +0 -110
  499. package/dist/tools/killTask.d.ts +0 -15
  500. package/dist/tools/listAgents.d.ts +0 -12
  501. package/dist/tools/listAgents.js +0 -42
  502. package/dist/tools/reconcileTasks.js +0 -352
  503. package/dist/tools/retryTask.js +0 -45
  504. package/dist/tools/runVerification.js +0 -36
  505. package/dist/tools/syncFile.js +0 -65
  506. package/docs/control-center/control-center-daily-driver.md +0 -211
  507. package/docs/control-center/control-center-mvp.md +0 -205
  508. package/docs/control-center/control-center-phase2.md +0 -159
  509. package/docs/execution-plan-2026-07-09.md +0 -315
  510. package/docs/release-v0.6.0.md +0 -71
  511. package/docs/release-v0.6.1.md +0 -75
  512. package/docs/release-v0.6.4.md +0 -45
  513. package/docs/roadmap-execution-and-acceptance.md +0 -365
  514. package/src/test/unit/acceptance-engine.test.ts +0 -261
  515. package/src/test/unit/agent-router.test.ts +0 -342
  516. package/src/test/unit/android-doctor.test.ts +0 -158
  517. package/src/test/unit/apply-patch.test.ts +0 -293
  518. package/src/test/unit/audit-checks.test.ts +0 -567
  519. package/src/test/unit/chinese-path.test.ts +0 -106
  520. package/src/test/unit/command-guard.test.ts +0 -221
  521. package/src/test/unit/create-task.test.ts +0 -255
  522. package/src/test/unit/diagnose-task.test.ts +0 -544
  523. package/src/test/unit/direct-guards.test.ts +0 -467
  524. package/src/test/unit/discovery-token-store.test.ts +0 -181
  525. package/src/test/unit/evidence-pack.test.ts +0 -251
  526. package/src/test/unit/get-task-status.test.ts +0 -203
  527. package/src/test/unit/get-task-summary.test.ts +0 -173
  528. package/src/test/unit/goal-graph.test.ts +0 -347
  529. package/src/test/unit/goal-progress.test.ts +0 -538
  530. package/src/test/unit/goal-report.test.ts +0 -189
  531. package/src/test/unit/goal-status.test.ts +0 -270
  532. package/src/test/unit/goal-store.test.ts +0 -318
  533. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  534. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  535. package/src/test/unit/handoff-export.test.ts +0 -295
  536. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  537. package/src/test/unit/logging.test.ts +0 -497
  538. package/src/test/unit/path-guard.test.ts +0 -180
  539. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  540. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  541. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  542. package/src/test/unit/release-gate.test.ts +0 -314
  543. package/src/test/unit/run-task-loop.test.ts +0 -425
  544. package/src/test/unit/safe-status.test.ts +0 -187
  545. package/src/test/unit/safe-views.test.ts +0 -184
  546. package/src/test/unit/schema-drift-check.test.ts +0 -260
  547. package/src/test/unit/sensitive-guard.test.ts +0 -124
  548. package/src/test/unit/spec-kit-import.test.ts +0 -429
  549. package/src/test/unit/subgoal-sync.test.ts +0 -236
  550. package/src/test/unit/sync-file.test.ts +0 -231
  551. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  552. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  553. package/src/test/unit/toolSearch.test.ts +0 -756
  554. package/src/test/unit/wait-for-task.test.ts +0 -176
  555. package/src/test/unit/watcher-status.test.ts +0 -190
  556. package/src/test/unit/worktree-manager.test.ts +0 -247
  557. package/src/tools/cancelTask.ts +0 -70
  558. package/src/tools/listAgents.ts +0 -54
  559. package/src/tools/retryTask.ts +0 -53
  560. package/src/tools/syncFile.ts +0 -122
  561. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  562. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  563. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  564. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  565. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  566. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  567. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  568. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  569. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  570. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  571. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  572. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  573. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  574. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  575. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  576. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  577. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  578. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  579. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  580. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  581. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  582. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  583. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  584. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  585. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  586. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  587. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  588. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  589. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  590. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  591. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
@@ -0,0 +1,231 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve, win32 } from "node:path";
3
+ import type { PatchWardenConfig } from "../config.js";
4
+ import { getRepoAllowedTestCommands, getRepoDirectAllowedCommands } from "../config.js";
5
+ import { stableJsonStringify } from "../utils/stableJson.js";
6
+ import type { ProjectPolicy } from "../policy/projectPolicy.js";
7
+
8
+ /**
9
+ * Version the security snapshot independently from the server/schema version.
10
+ * Changing the fields or their canonicalization should invalidate old records.
11
+ */
12
+ export const ASSESSMENT_SECURITY_SNAPSHOT_VERSION = "assessment-security-v2";
13
+
14
+ export type SecuritySnapshotCategory =
15
+ | "schema_epoch"
16
+ | "tool_profile"
17
+ | "tool_manifest"
18
+ | "workspace_root"
19
+ | "repo_boundary"
20
+ | "agent_launch"
21
+ | "allowed_commands"
22
+ | "repo_allowed_test_commands"
23
+ | "direct_allowed_commands"
24
+ | "repo_direct_allowed_commands"
25
+ | "sensitive_path_rules"
26
+ | "protected_paths"
27
+ | "project_policy"
28
+ | "risk_rules"
29
+ | "confirmation_policy"
30
+ | "task_parameters"
31
+ | "release_protection"
32
+ | "direct_profile"
33
+ | "assessment_ttl"
34
+ | "timeout_policy";
35
+
36
+ export interface AssessmentSecuritySnapshotInput {
37
+ config: PatchWardenConfig;
38
+ schemaEpoch: string;
39
+ toolProfile: string;
40
+ toolManifestSha256: string;
41
+ agent: string;
42
+ repoPath: string;
43
+ changePolicy?: string | null;
44
+ template?: string | null;
45
+ verifyCommands?: string[];
46
+ testCommand?: string | null;
47
+ taskTimeoutSeconds?: number | null;
48
+ scope?: string[];
49
+ forbidden?: string[];
50
+ verification?: string[];
51
+ doneEvidence?: string[];
52
+ riskRulesVersion?: string;
53
+ projectPolicy?: ProjectPolicy | null;
54
+ projectPolicyValid?: boolean | null;
55
+ projectPolicyIssues?: Array<{ code: string; severity: string; field: string }>;
56
+ }
57
+
58
+ export interface AssessmentSecuritySnapshot {
59
+ assessment_security_snapshot_version: string;
60
+ components: Record<SecuritySnapshotCategory, unknown>;
61
+ }
62
+
63
+ export interface AssessmentSecuritySnapshotComparison {
64
+ equal: boolean;
65
+ expected_hash: string;
66
+ actual_hash: string;
67
+ changed_field_names: SecuritySnapshotCategory[];
68
+ }
69
+
70
+ function canonicalPath(value: string): string {
71
+ const rawValue = String(value);
72
+ if (/^[a-z]:[\\/]/i.test(rawValue)) {
73
+ return win32.normalize(rawValue).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
74
+ }
75
+ const normalized = resolve(rawValue).replace(/\\/g, "/").replace(/\/+$/, "");
76
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
77
+ }
78
+
79
+ function sortedUnique(values: readonly string[] | undefined): string[] {
80
+ return [...new Set((values || []).map((value) => String(value).trim()))]
81
+ .filter(Boolean)
82
+ .sort((left, right) => left.localeCompare(right));
83
+ }
84
+
85
+ function sortedUniqueTaskPaths(values: readonly string[] | undefined): string[] {
86
+ return sortedUnique(values?.map((value) => {
87
+ const normalized = String(value).replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/{2,}/g, "/");
88
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
89
+ }));
90
+ }
91
+
92
+ function sortedAgentConfig(config: PatchWardenConfig): Record<string, unknown> {
93
+ return Object.fromEntries(Object.entries(config.agents)
94
+ .sort(([left], [right]) => left.localeCompare(right))
95
+ .map(([name, agent]) => [name, {
96
+ command: agent.command,
97
+ args: [...agent.args],
98
+ adapter: agent.adapter || null,
99
+ model: agent.model || null,
100
+ envAllowlist: sortedUnique(agent.envAllowlist),
101
+ }]));
102
+ }
103
+
104
+ /** Build only security-relevant configuration; runtime state is deliberately absent. */
105
+ export function buildAssessmentSecuritySnapshot(input: AssessmentSecuritySnapshotInput): AssessmentSecuritySnapshot {
106
+ const { config } = input;
107
+ const repoPath = canonicalPath(input.repoPath);
108
+ const components: Record<SecuritySnapshotCategory, unknown> = {
109
+ schema_epoch: input.schemaEpoch,
110
+ tool_profile: input.toolProfile,
111
+ tool_manifest: input.toolManifestSha256,
112
+ workspace_root: canonicalPath(config.workspaceRoot),
113
+ repo_boundary: {
114
+ workspace_root: canonicalPath(config.workspaceRoot),
115
+ repo_path: repoPath,
116
+ repo_aliases: Object.fromEntries(Object.entries(config.repoAliases || {})
117
+ .sort(([left], [right]) => left.localeCompare(right))
118
+ .map(([key, value]) => [key, canonicalPath(resolve(config.workspaceRoot, value))])),
119
+ },
120
+ agent_launch: {
121
+ selected_agent: input.agent,
122
+ configured_agents: sortedAgentConfig(config),
123
+ },
124
+ allowed_commands: {
125
+ configured: sortedUnique(config.allowedTestCommands),
126
+ project: sortedUnique(input.projectPolicy?.allowed_commands),
127
+ },
128
+ repo_allowed_test_commands: sortedUnique(getRepoAllowedTestCommands(config, repoPath)),
129
+ direct_allowed_commands: sortedUnique(config.directAllowedCommands),
130
+ repo_direct_allowed_commands: sortedUnique(getRepoDirectAllowedCommands(config, repoPath)),
131
+ sensitive_path_rules: "sensitive-path-rules-v1",
132
+ protected_paths: sortedUnique(input.projectPolicy?.protected_paths || [
133
+ ".env", ".env.*", ".ssh", ".npmrc", ".pypirc", "patchwarden.config.json",
134
+ ]),
135
+ project_policy: {
136
+ valid: input.projectPolicyValid ?? null,
137
+ effective_policy: input.projectPolicy || null,
138
+ issues: [...(input.projectPolicyIssues || [])]
139
+ .map((issue) => ({ code: issue.code, severity: issue.severity, field: issue.field }))
140
+ .sort((left, right) => stableJsonStringify(left).localeCompare(stableJsonStringify(right))),
141
+ },
142
+ risk_rules: {
143
+ implementation: input.riskRulesVersion || "risk-engine-v1",
144
+ project_high_risk_commands: sortedUnique(input.projectPolicy?.high_risk_commands),
145
+ },
146
+ confirmation_policy: {
147
+ change_policy: input.changePolicy || "repo_scoped_changes",
148
+ template: input.template || null,
149
+ },
150
+ task_parameters: {
151
+ test_command: input.testCommand || null,
152
+ verify_commands: sortedUnique(input.verifyCommands),
153
+ timeout_seconds: input.taskTimeoutSeconds ?? config.defaultTaskTimeoutSeconds,
154
+ scope: sortedUniqueTaskPaths(input.scope),
155
+ forbidden: sortedUniqueTaskPaths(input.forbidden),
156
+ verification: sortedUnique(input.verification),
157
+ done_evidence: sortedUniqueTaskPaths(input.doneEvidence),
158
+ },
159
+ release_protection: {
160
+ high_risk_commands: sortedUnique(input.projectPolicy?.high_risk_commands || [
161
+ "npm publish", "git push", "git tag", "gh release create",
162
+ ]),
163
+ release_mode: input.projectPolicy?.release_mode || {
164
+ version_source: "package.json",
165
+ required_commands: ["npm run build", "npm test"],
166
+ },
167
+ artifact_rules: "artifact-rules-v1",
168
+ },
169
+ direct_profile: Boolean(config.enableDirectProfile),
170
+ assessment_ttl: config.assessmentTtlSeconds,
171
+ timeout_policy: {
172
+ default_task_timeout_seconds: config.defaultTaskTimeoutSeconds,
173
+ max_task_timeout_seconds: config.maxTaskTimeoutSeconds,
174
+ },
175
+ };
176
+
177
+ return {
178
+ assessment_security_snapshot_version: ASSESSMENT_SECURITY_SNAPSHOT_VERSION,
179
+ components,
180
+ };
181
+ }
182
+
183
+ /** Recursively sort object keys while preserving array order where it matters. */
184
+ export function canonicalizeAssessmentSecuritySnapshot(snapshot: AssessmentSecuritySnapshot): AssessmentSecuritySnapshot {
185
+ const canonicalize = (value: unknown): unknown => {
186
+ if (Array.isArray(value)) return value.map(canonicalize);
187
+ if (value !== null && typeof value === "object") {
188
+ return Object.fromEntries(Object.entries(value as Record<string, unknown>)
189
+ .sort(([left], [right]) => left.localeCompare(right))
190
+ .map(([key, entry]) => [key, canonicalize(entry)]));
191
+ }
192
+ return value;
193
+ };
194
+ return canonicalize(snapshot) as AssessmentSecuritySnapshot;
195
+ }
196
+
197
+ export function hashAssessmentSecuritySnapshot(snapshot: AssessmentSecuritySnapshot): string {
198
+ return createHash("sha256")
199
+ .update(stableJsonStringify(canonicalizeAssessmentSecuritySnapshot(snapshot)))
200
+ .digest("hex");
201
+ }
202
+
203
+ export function getAssessmentSecuritySnapshotComponentHashes(
204
+ snapshot: AssessmentSecuritySnapshot,
205
+ ): Record<SecuritySnapshotCategory, string> {
206
+ return Object.fromEntries(Object.entries(snapshot.components).map(([name, value]) => [
207
+ name,
208
+ createHash("sha256").update(stableJsonStringify(value)).digest("hex"),
209
+ ])) as Record<SecuritySnapshotCategory, string>;
210
+ }
211
+
212
+ export function compareAssessmentSecuritySnapshots(
213
+ expected: AssessmentSecuritySnapshot,
214
+ actual: AssessmentSecuritySnapshot,
215
+ ): AssessmentSecuritySnapshotComparison {
216
+ const expectedHash = hashAssessmentSecuritySnapshot(expected);
217
+ const actualHash = hashAssessmentSecuritySnapshot(actual);
218
+ const expectedComponents = getAssessmentSecuritySnapshotComponentHashes(expected);
219
+ const actualComponents = getAssessmentSecuritySnapshotComponentHashes(actual);
220
+ const changed = (Object.keys(actualComponents) as SecuritySnapshotCategory[])
221
+ .filter((name) => expectedComponents[name] !== actualComponents[name]);
222
+ if (expected.assessment_security_snapshot_version !== actual.assessment_security_snapshot_version) {
223
+ changed.unshift("schema_epoch");
224
+ }
225
+ return {
226
+ equal: expectedHash === actualHash,
227
+ expected_hash: expectedHash,
228
+ actual_hash: actualHash,
229
+ changed_field_names: [...new Set(changed)],
230
+ };
231
+ }
package/src/config.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
- import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { validateWorkspaceRoot } from "./security/workspaceRootGuard.js";
3
4
 
4
5
  // ── Type definitions ──────────────────────────────────────────────
5
6
 
6
7
  export interface AgentConfig {
7
8
  command: string;
8
9
  args: string[];
10
+ envAllowlist?: string[];
11
+ adapter?: string;
12
+ model?: string;
9
13
  }
10
14
 
11
15
  export interface PatchWardenConfig {
@@ -31,6 +35,12 @@ export interface PatchWardenConfig {
31
35
  agentAssessmentMaxOutputBytes?: number;
32
36
  agentAssessmentAgentName?: string;
33
37
  enableDirectProfile?: boolean;
38
+ tunnelClientPath?: string;
39
+ tunnelProxy?: {
40
+ scope: "shared" | "separate";
41
+ core: { mode: "environment" | "none" | "manual"; url?: string };
42
+ direct: { mode: "environment" | "none" | "manual"; url?: string };
43
+ };
34
44
  directSessionsDir: string;
35
45
  directSessionTtlSeconds: number;
36
46
  directMaxPatchBytes: number;
@@ -47,16 +57,7 @@ const DEFAULT_CONFIG: PatchWardenConfig = {
47
57
  tasksDir: ".patchwarden/tasks",
48
58
  assessmentsDir: ".patchwarden/assessments",
49
59
  assessmentTtlSeconds: 3600,
50
- agents: {
51
- codex: {
52
- command: "codex",
53
- args: ["exec", "--cd", "{repo}", "{prompt}"],
54
- },
55
- opencode: {
56
- command: "opencode",
57
- args: ["run", "{prompt}"],
58
- },
59
- },
60
+ agents: {},
60
61
  allowedTestCommands: [
61
62
  "npm test",
62
63
  "npm run test",
@@ -175,6 +176,11 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
175
176
  if (!config.workspaceRoot || typeof config.workspaceRoot !== "string") {
176
177
  throw new Error("workspaceRoot must be a non-empty string");
177
178
  }
179
+ const workspaceValidation = validateWorkspaceRoot(config.workspaceRoot);
180
+ if (!workspaceValidation.ok) {
181
+ throw new Error(`Invalid workspaceRoot: ${workspaceValidation.reason} (${workspaceValidation.path})`);
182
+ }
183
+ config.workspaceRoot = workspaceValidation.path;
178
184
  if (!config.plansDir || typeof config.plansDir !== "string") {
179
185
  throw new Error("plansDir must be a non-empty string");
180
186
  }
@@ -190,6 +196,20 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
190
196
  if (!config.agents || typeof config.agents !== "object") {
191
197
  throw new Error("agents must be an object");
192
198
  }
199
+ const agents: Record<string, AgentConfig> = {};
200
+ for (const [agentName, agent] of Object.entries(config.agents)) {
201
+ if (!agent || typeof agent !== "object" || typeof agent.command !== "string" || !Array.isArray(agent.args)) {
202
+ throw new Error(`agents["${agentName}"] must define command and args`);
203
+ }
204
+ const envAllowlist = agent.envAllowlist ?? [];
205
+ if (!Array.isArray(envAllowlist) || envAllowlist.some((name) => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) {
206
+ throw new Error(`agents["${agentName}"].envAllowlist must contain only environment variable names`);
207
+ }
208
+ const blockedNames = new Set(["CONTROL_PLANE_API_KEY", "PATCHWARDEN_OWNER_TOKEN", config.http?.ownerTokenEnv?.toUpperCase()]);
209
+ const blocked = envAllowlist.find((name) => blockedNames.has(name.toUpperCase()));
210
+ if (blocked) throw new Error(`agents["${agentName}"].envAllowlist cannot include reserved variable "${blocked}"`);
211
+ agents[agentName] = { ...agent, args: [...agent.args], envAllowlist: [...new Set(envAllowlist)] };
212
+ }
193
213
  if (!Array.isArray(config.allowedTestCommands)) {
194
214
  throw new Error("allowedTestCommands must be an array");
195
215
  }
@@ -263,8 +283,14 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
263
283
  if (config.http.host !== undefined && typeof config.http.host !== "string") {
264
284
  throw new Error("http.host must be a string");
265
285
  }
266
- if (config.http.ownerTokenEnv !== undefined && typeof config.http.ownerTokenEnv !== "string") {
267
- throw new Error("http.ownerTokenEnv must be a string");
286
+ if (
287
+ config.http.ownerTokenEnv !== undefined
288
+ && (
289
+ typeof config.http.ownerTokenEnv !== "string"
290
+ || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.http.ownerTokenEnv)
291
+ )
292
+ ) {
293
+ throw new Error("http.ownerTokenEnv must be a valid environment variable name");
268
294
  }
269
295
  }
270
296
  if (config.enableRunTaskTool !== undefined && typeof config.enableRunTaskTool !== "boolean") {
@@ -289,6 +315,36 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
289
315
  if (config.enableDirectProfile !== undefined && typeof config.enableDirectProfile !== "boolean") {
290
316
  throw new Error("enableDirectProfile must be a boolean");
291
317
  }
318
+ if (config.tunnelClientPath !== undefined) {
319
+ if (typeof config.tunnelClientPath !== "string" || !isAbsolute(config.tunnelClientPath)) {
320
+ throw new Error("tunnelClientPath must be an absolute path");
321
+ }
322
+ if (basename(config.tunnelClientPath).toLowerCase() !== "tunnel-client.exe") {
323
+ throw new Error("tunnelClientPath must point to tunnel-client.exe");
324
+ }
325
+ }
326
+ if (config.tunnelProxy !== undefined) {
327
+ if (!config.tunnelProxy || typeof config.tunnelProxy !== "object" || !["shared", "separate"].includes(config.tunnelProxy.scope)) {
328
+ throw new Error("tunnelProxy.scope must be shared or separate");
329
+ }
330
+ for (const key of ["core", "direct"] as const) {
331
+ const endpoint = config.tunnelProxy[key];
332
+ if (!endpoint || typeof endpoint !== "object" || !["environment", "none", "manual"].includes(endpoint.mode)) {
333
+ throw new Error(`tunnelProxy.${key}.mode must be environment, none, or manual`);
334
+ }
335
+ if (endpoint.mode === "manual") {
336
+ if (typeof endpoint.url !== "string") throw new Error(`tunnelProxy.${key}.url is required in manual mode`);
337
+ let parsed: URL;
338
+ try { parsed = new URL(endpoint.url); } catch { throw new Error(`tunnelProxy.${key}.url must be a valid URL`); }
339
+ if (!["http:", "https:", "socks5:"].includes(parsed.protocol)) {
340
+ throw new Error(`tunnelProxy.${key}.url must use http, https, or socks5`);
341
+ }
342
+ if (parsed.username || parsed.password) throw new Error(`tunnelProxy.${key}.url must not contain credentials`);
343
+ } else if (endpoint.url !== undefined) {
344
+ throw new Error(`tunnelProxy.${key}.url is only allowed in manual mode`);
345
+ }
346
+ }
347
+ }
292
348
  if (!config.directSessionsDir || typeof config.directSessionsDir !== "string") {
293
349
  throw new Error("directSessionsDir must be a non-empty string");
294
350
  }
@@ -331,6 +387,7 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
331
387
 
332
388
  return {
333
389
  ...config,
390
+ agents,
334
391
  workspaceRoot: resolve(config.workspaceRoot),
335
392
  allowedTestCommands: [...new Set(config.allowedTestCommands.map((command) => command.trim()))],
336
393
  repoAllowedTestCommands,
@@ -0,0 +1,40 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { buildChildEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
4
+
5
+ const FILE_MANAGER_ENVIRONMENT = [
6
+ "DISPLAY",
7
+ "WAYLAND_DISPLAY",
8
+ "DBUS_SESSION_BUS_ADDRESS",
9
+ "XDG_RUNTIME_DIR",
10
+ ] as const;
11
+
12
+ /** Launch a trusted platform file manager without inheriting ambient secrets. */
13
+ export function launchFileManager(target: string, cwd: string): boolean {
14
+ try {
15
+ const env = buildChildEnvironment({ cwd, allowedNames: FILE_MANAGER_ENVIRONMENT });
16
+ const requested = fileManagerCommand(env);
17
+ const command = resolveTrustedExecutable(requested, cwd, { pathValue: env.PATH });
18
+ const child = spawn(command, [target], {
19
+ cwd,
20
+ detached: true,
21
+ env,
22
+ stdio: "ignore",
23
+ windowsHide: true,
24
+ });
25
+ child.on("error", () => { /* opening a convenience viewer is best effort */ });
26
+ child.unref();
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function fileManagerCommand(env: NodeJS.ProcessEnv): string {
34
+ if (process.platform === "win32") {
35
+ const windowsRoot = env.SystemRoot || env.WINDIR;
36
+ return windowsRoot ? join(windowsRoot, "explorer.exe") : "explorer.exe";
37
+ }
38
+ if (process.platform === "darwin") return "open";
39
+ return "xdg-open";
40
+ }
@@ -7,11 +7,29 @@
7
7
  * action must pass through `checkControlToken` first.
8
8
  */
9
9
  import { type IncomingMessage } from "node:http";
10
+ import { firstHeaderValue, timingSafeStringEqual } from "../../security/secretComparison.js";
11
+ import { isTrustedLoopbackHostHeader } from "../../security/loopbackHost.js";
10
12
  import { controlToken } from "../shared.js";
11
13
 
12
14
  export function checkControlToken(req: IncomingMessage): boolean {
13
- const header = req.headers["x-patchwarden-control-token"];
14
- const provided = Array.isArray(header) ? header[0] : header;
15
- if (typeof provided !== "string" || provided.length === 0) return false;
16
- return provided === controlToken;
15
+ const provided = firstHeaderValue(req.headers["x-patchwarden-control-token"]);
16
+ return provided.length > 0 && timingSafeStringEqual(provided, controlToken);
17
+ }
18
+
19
+ /**
20
+ * Reject non-loopback Host headers before exposing the browser-readable
21
+ * control token. Binding to 127.0.0.1 alone does not prevent DNS rebinding.
22
+ */
23
+ export function isTrustedControlHost(
24
+ req: IncomingMessage,
25
+ expectedPort: number,
26
+ ): boolean {
27
+ return isTrustedControlHostHeader(req.headers.host, expectedPort);
28
+ }
29
+
30
+ export function isTrustedControlHostHeader(
31
+ value: string | string[] | undefined,
32
+ expectedPort: number,
33
+ ): boolean {
34
+ return isTrustedLoopbackHostHeader(value, expectedPort);
17
35
  }
@@ -5,7 +5,7 @@
5
5
  * assets, css) with path-traversal protection, plus the inline SVG favicon.
6
6
  * Content types are mapped from `CONTENT_TYPES` in shared.ts.
7
7
  */
8
- import { existsSync, readFileSync, statSync } from "node:fs";
8
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
9
9
  import { extname, isAbsolute, relative, resolve } from "node:path";
10
10
  import { type ServerResponse } from "node:http";
11
11
  import {
@@ -51,9 +51,22 @@ export function serveStatic(res: ServerResponse, urlPath: string): void {
51
51
  sendJson(res, 404, { error: "Not found" });
52
52
  return;
53
53
  }
54
- const ext = extname(candidate).toLowerCase();
54
+ // Resolve links before reading. Lexical `..` checks alone do not stop a
55
+ // symlink or junction inside `ui/` from pointing outside the bundle.
56
+ const realRoot = realpathSync(uiRoot);
57
+ const realCandidate = realpathSync(candidate);
58
+ const realRelative = relative(realRoot, realCandidate);
59
+ if (realRelative === "" || realRelative.startsWith("..") || isAbsolute(realRelative)) {
60
+ sendJson(res, 403, { error: "Forbidden" });
61
+ return;
62
+ }
63
+ if (!statSync(realCandidate).isFile()) {
64
+ sendJson(res, 404, { error: "Not found" });
65
+ return;
66
+ }
67
+ const ext = extname(realCandidate).toLowerCase();
55
68
  const contentType = CONTENT_TYPES[ext] || "application/octet-stream";
56
- const content = readFileSync(candidate);
69
+ const content = readFileSync(realCandidate);
57
70
  res.writeHead(200, { "Content-Type": contentType });
58
71
  res.end(content);
59
72
  } catch (err) {