patchwarden 1.5.0 → 1.6.1

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