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
@@ -15,44 +15,60 @@
15
15
  * or: npm run start:http
16
16
  */
17
17
  import { createServer } from "node:http";
18
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { existsSync, readFileSync } from "node:fs";
19
19
  import { join, resolve } from "node:path";
20
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
21
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
22
22
  import { loadConfig, getTasksDir } from "./config.js";
23
23
  import { registerTools } from "./tools/registry.js";
24
- import { healthCheck } from "./tools/healthCheck.js";
24
+ import { healthCheck } from "./tools/diagnostics/healthCheck.js";
25
25
  import { getToolCatalogSnapshot } from "./tools/registry.js";
26
26
  import { PATCHWARDEN_VERSION } from "./version.js";
27
+ import { logger } from "./logging.js";
28
+ import { firstHeaderValue, timingSafeStringEqual } from "./security/secretComparison.js";
29
+ import { redactSensitiveContent } from "./security/contentRedaction.js";
30
+ import { atomicWriteJsonFileSync } from "./utils/atomicFile.js";
31
+ import { mutateTaskStatus } from "./runner/taskStatusStore.js";
32
+ import { isTrustedLoopbackHostHeader } from "./security/loopbackHost.js";
27
33
  // ── Bootstrap ─────────────────────────────────────────────────────
28
34
  const config = loadConfig();
29
35
  const port = parseInt(process.env.PATCHWARDEN_HTTP_PORT || "") ||
30
36
  config.httpPort ||
31
37
  7331;
32
38
  const host = "127.0.0.1";
33
- console.error(`[patchwarden-http] Workspace: ${config.workspaceRoot}`);
34
- console.error(`[patchwarden-http] Listening: http://${host}:${port}/mcp`);
35
- console.error(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed to network`);
39
+ logger.info(`[patchwarden-http] Workspace: ${config.workspaceRoot}`);
40
+ logger.info(`[patchwarden-http] Listening: http://${host}:${port}/mcp`);
41
+ logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed to network`);
36
42
  // ── Owner token (optional) ────────────────────────────────────────
37
43
  const httpCfg = config.http || {};
38
44
  const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
39
45
  const ownerToken = process.env[ownerTokenEnv] || "";
46
+ const MAX_ADMIN_BODY_BYTES = 64 * 1024;
47
+ const MAX_ACCEPTANCE_NOTES_CHARS = 10_000;
48
+ const REVIEWABLE_TASK_STATUSES = new Set([
49
+ "done_by_agent",
50
+ "done",
51
+ "accepted",
52
+ "rejected",
53
+ "needs_fix",
54
+ "blocked",
55
+ ]);
40
56
  if (ownerToken) {
41
- console.error(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
57
+ logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
42
58
  }
43
59
  else {
44
- console.error(`[patchwarden-http] ⚠️ No owner token set — all local requests accepted`);
60
+ logger.info(`[patchwarden-http] ⚠️ No owner token set — all local requests accepted`);
45
61
  }
46
62
  function checkOwnerToken(req) {
47
63
  if (!ownerToken)
48
64
  return true; // no token configured — allow all
49
- const authHeader = req.headers["authorization"] || "";
50
- const customHeader = req.headers["x-patchwarden-token"] || "";
65
+ const authHeader = firstHeaderValue(req.headers["authorization"]);
66
+ const customHeader = firstHeaderValue(req.headers["x-patchwarden-token"]);
51
67
  if (authHeader.startsWith("Bearer ")) {
52
- return authHeader.slice(7) === ownerToken;
68
+ return timingSafeStringEqual(authHeader.slice(7), ownerToken);
53
69
  }
54
- if (typeof customHeader === "string" && customHeader.length > 0) {
55
- return customHeader === ownerToken;
70
+ if (customHeader.length > 0) {
71
+ return timingSafeStringEqual(customHeader, ownerToken);
56
72
  }
57
73
  return false;
58
74
  }
@@ -68,12 +84,23 @@ function handleAcceptance(taskId, status, body) {
68
84
  throw Object.assign(new Error(`Task "${taskId}" not found.`), { statusCode: 404 });
69
85
  }
70
86
  let notes = "";
71
- try {
72
- const parsed = JSON.parse(body || "{}");
73
- notes = String(parsed.notes || parsed.reason || "");
74
- }
75
- catch {
76
- // no body or invalid JSON — notes stays empty
87
+ if (body.trim()) {
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(body);
91
+ }
92
+ catch {
93
+ throw httpError(400, "Acceptance body must be valid JSON.");
94
+ }
95
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
96
+ throw httpError(400, "Acceptance body must be a JSON object.");
97
+ }
98
+ const record = parsed;
99
+ const rawNotes = record.notes ?? record.reason ?? "";
100
+ if (typeof rawNotes !== "string") {
101
+ throw httpError(400, "Acceptance notes or reason must be a string.");
102
+ }
103
+ notes = redactSensitiveContent(rawNotes).content.slice(0, MAX_ACCEPTANCE_NOTES_CHARS);
77
104
  }
78
105
  const acceptance = {
79
106
  status,
@@ -81,22 +108,67 @@ function handleAcceptance(taskId, status, body) {
81
108
  reviewer: "human",
82
109
  notes,
83
110
  };
84
- writeFileSync(filePath, JSON.stringify(acceptance, null, 2), "utf-8");
85
- // Also update the task's status.json with acceptance_status
111
+ // Commit the review artifact and status annotation while holding the same
112
+ // status lock used by the runner, cancel, and audit paths.
86
113
  const statusFile = join(taskDir, "status.json");
87
- if (existsSync(statusFile)) {
88
- try {
89
- const current = JSON.parse(readFileSync(statusFile, "utf-8"));
90
- current.acceptance_status = status;
91
- current.acceptance_reviewed_at = acceptance.reviewed_at;
92
- writeFileSync(statusFile, JSON.stringify(current, null, 2), "utf-8");
93
- }
94
- catch {
95
- // status.json update is best-effort
96
- }
114
+ if (!existsSync(statusFile)) {
115
+ throw httpError(409, `Task "${taskId}" has no status.json to review.`);
97
116
  }
117
+ mutateTaskStatus(statusFile, (current) => {
118
+ if (!REVIEWABLE_TASK_STATUSES.has(String(current.status || ""))) {
119
+ throw httpError(409, `Task "${taskId}" is not in a reviewable terminal state.`);
120
+ }
121
+ atomicWriteJsonFileSync(filePath, acceptance);
122
+ const next = {
123
+ ...current,
124
+ acceptance_status: status,
125
+ acceptance_reviewed_at: acceptance.reviewed_at,
126
+ updated_at: new Date().toISOString(),
127
+ };
128
+ return { next, result: next };
129
+ });
98
130
  return acceptance;
99
131
  }
132
+ function httpError(statusCode, message) {
133
+ return Object.assign(new Error(message), { statusCode });
134
+ }
135
+ function readBoundedRequestBody(req, maxBytes) {
136
+ const declaredLength = Number(firstHeaderValue(req.headers["content-length"]));
137
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
138
+ req.resume();
139
+ return Promise.reject(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
140
+ }
141
+ return new Promise((resolveBody, rejectBody) => {
142
+ const chunks = [];
143
+ let total = 0;
144
+ let settled = false;
145
+ req.on("data", (chunk) => {
146
+ if (settled)
147
+ return;
148
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
149
+ total += buffer.length;
150
+ if (total > maxBytes) {
151
+ settled = true;
152
+ req.resume();
153
+ rejectBody(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
154
+ return;
155
+ }
156
+ chunks.push(buffer);
157
+ });
158
+ req.on("end", () => {
159
+ if (!settled) {
160
+ settled = true;
161
+ resolveBody(Buffer.concat(chunks).toString("utf-8"));
162
+ }
163
+ });
164
+ req.on("error", (error) => {
165
+ if (!settled) {
166
+ settled = true;
167
+ rejectBody(error);
168
+ }
169
+ });
170
+ });
171
+ }
100
172
  function readAcceptance(taskId) {
101
173
  const filePath = getAcceptancePath(taskId);
102
174
  if (!existsSync(filePath)) {
@@ -123,7 +195,9 @@ async function handleMcpRequest(req, res) {
123
195
  await transport.handleRequest(req, res);
124
196
  }
125
197
  catch (err) {
126
- console.error("[patchwarden-http] Request error:", err);
198
+ logger.error("[patchwarden-http] Request error", {
199
+ error: err instanceof Error ? err.message : String(err),
200
+ });
127
201
  if (!res.headersSent) {
128
202
  res.writeHead(500, { "Content-Type": "application/json" });
129
203
  res.end(JSON.stringify({ error: "Internal server error" }));
@@ -161,6 +235,15 @@ function parseAdminUrl(url) {
161
235
  }
162
236
  // ── HTTP server ───────────────────────────────────────────────────
163
237
  const httpServer = createServer(async (req, res) => {
238
+ if (!isTrustedLoopbackHostHeader(req.headers.host, port)) {
239
+ res.writeHead(421, {
240
+ "Content-Type": "application/json; charset=utf-8",
241
+ "Cache-Control": "no-store",
242
+ "X-Content-Type-Options": "nosniff",
243
+ });
244
+ res.end(JSON.stringify({ error: "Untrusted Host header" }));
245
+ return;
246
+ }
164
247
  // Health check endpoints
165
248
  if (req.method === "GET" && (req.url === "/healthz" || req.url === "/readyz")) {
166
249
  const health = healthCheck(getToolCatalogSnapshot());
@@ -185,11 +268,7 @@ const httpServer = createServer(async (req, res) => {
185
268
  return;
186
269
  }
187
270
  if ((admin.action === "accept" || admin.action === "reject") && req.method === "POST") {
188
- const chunks = [];
189
- for await (const chunk of req) {
190
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
191
- }
192
- const body = Buffer.concat(chunks).toString("utf-8");
271
+ const body = await readBoundedRequestBody(req, MAX_ADMIN_BODY_BYTES);
193
272
  const acceptance = handleAcceptance(admin.taskId, admin.action === "accept" ? "accepted" : "rejected", body);
194
273
  res.writeHead(200, { "Content-Type": "application/json" });
195
274
  res.end(JSON.stringify(acceptance, null, 2));
@@ -199,9 +278,12 @@ const httpServer = createServer(async (req, res) => {
199
278
  res.end(JSON.stringify({ error: "Method not allowed for this admin endpoint." }));
200
279
  }
201
280
  catch (err) {
202
- const statusCode = err.statusCode || 500;
281
+ const statusCode = err && typeof err === "object" && "statusCode" in err
282
+ ? Number(err.statusCode) || 500
283
+ : 500;
284
+ const message = err instanceof Error ? err.message : "Internal server error";
203
285
  res.writeHead(statusCode, { "Content-Type": "application/json" });
204
- res.end(JSON.stringify({ error: err.message || "Internal server error" }));
286
+ res.end(JSON.stringify({ error: message }));
205
287
  }
206
288
  return;
207
289
  }
@@ -233,21 +315,21 @@ const httpServer = createServer(async (req, res) => {
233
315
  // ── Start ─────────────────────────────────────────────────────────
234
316
  httpServer.on("error", (err) => {
235
317
  if (err.code === "EADDRINUSE") {
236
- console.error(`[patchwarden-http] Fatal: port ${port} is already in use on ${host}.`);
237
- console.error("[patchwarden-http] Stop the other PatchWarden HTTP instance or change httpPort in patchwarden.config.json.");
318
+ logger.fatal(`[patchwarden-http] Fatal: port ${port} is already in use on ${host}.`);
319
+ logger.fatal("[patchwarden-http] Stop the other PatchWarden HTTP instance or change httpPort in patchwarden.config.json.");
238
320
  }
239
321
  else {
240
- console.error(`[patchwarden-http] Fatal: ${err.message}`);
322
+ logger.fatal(`[patchwarden-http] Fatal: ${err.message}`);
241
323
  }
242
324
  process.exit(1);
243
325
  });
244
326
  httpServer.listen(port, host, () => {
245
- console.error(`[patchwarden-http] ✅ Ready`);
246
- console.error(`[patchwarden-http] Admin: http://${host}:${port}/admin/tasks/:id/accept`);
327
+ logger.info(`[patchwarden-http] ✅ Ready`);
328
+ logger.info(`[patchwarden-http] Admin: http://${host}:${port}/admin/tasks/:id/accept`);
247
329
  });
248
330
  // Graceful shutdown
249
331
  process.on("SIGINT", () => {
250
- console.error("[patchwarden-http] Shutting down...");
332
+ logger.info("[patchwarden-http] Shutting down...");
251
333
  httpServer.close(() => process.exit(0));
252
334
  });
253
335
  process.on("SIGTERM", () => {
package/dist/index.js CHANGED
@@ -11,14 +11,17 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
11
11
  import { loadConfig } from "./config.js";
12
12
  import { registerTools } from "./tools/registry.js";
13
13
  import { PATCHWARDEN_VERSION } from "./version.js";
14
+ import { logger } from "./logging.js";
14
15
  const config = loadConfig();
15
- console.error(`[patchwarden] Workspace: ${config.workspaceRoot}`);
16
- console.error(`[patchwarden] Transport: stdio`);
16
+ logger.info(`[patchwarden] Workspace: ${config.workspaceRoot}`);
17
+ logger.info("[patchwarden] Transport: stdio");
17
18
  const server = new Server({ name: "patchwarden", version: PATCHWARDEN_VERSION }, { capabilities: { tools: {} } });
18
19
  registerTools(server);
19
20
  const transport = new StdioServerTransport();
20
21
  server.connect(transport).catch((err) => {
21
- console.error("[patchwarden] Fatal:", err);
22
+ logger.fatal("[patchwarden] Fatal", {
23
+ error: err instanceof Error ? err.message : String(err),
24
+ });
22
25
  process.exit(1);
23
26
  });
24
- console.error("[patchwarden] MCP server ready on stdio");
27
+ logger.info("[patchwarden] MCP server ready on stdio");
package/dist/logging.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type LogLevel = "info" | "warn" | "error" | "audit";
1
+ export type LogLevel = "info" | "warn" | "error" | "fatal" | "audit";
2
2
  export interface LogEntry {
3
3
  timestamp: string;
4
4
  level: LogLevel;
@@ -39,6 +39,12 @@ export declare class Logger {
39
39
  info(message: string, context?: Record<string, unknown>): void;
40
40
  warn(message: string, context?: Record<string, unknown>): void;
41
41
  error(message: string, context?: Record<string, unknown>): void;
42
+ /**
43
+ * Emit a fatal-level log entry. Use this immediately before a process
44
+ * exits with a non-zero status, to record the unrecoverable condition.
45
+ * Level is higher than `error`.
46
+ */
47
+ fatal(message: string, context?: Record<string, unknown>): void;
42
48
  /**
43
49
  * Emit a tool-call audit log entry.
44
50
  *
package/dist/logging.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
- import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { redactSensitiveContent } from "./security/contentRedaction.js";
5
- import { stableJsonStringify } from "./tools/toolRegistry.js";
5
+ import { stableJsonStringify } from "./utils/stableJson.js";
6
6
  import { getConfig } from "./config.js";
7
+ import { appendBoundedTextFileSync } from "./utils/boundedFile.js";
8
+ const MAX_INVOCATION_LOG_BYTES = 5 * 1024 * 1024;
7
9
  // ── Helpers ───────────────────────────────────────────────────────
8
10
  /**
9
11
  * Returns true when PATCHWARDEN_VERBOSE_LOG is set to "true".
@@ -51,6 +53,14 @@ export class Logger {
51
53
  error(message, context) {
52
54
  emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
53
55
  }
56
+ /**
57
+ * Emit a fatal-level log entry. Use this immediately before a process
58
+ * exits with a non-zero status, to record the unrecoverable condition.
59
+ * Level is higher than `error`.
60
+ */
61
+ fatal(message, context) {
62
+ emit({ timestamp: new Date().toISOString(), level: "fatal", message, ...context });
63
+ }
54
64
  /**
55
65
  * Emit a tool-call audit log entry.
56
66
  *
@@ -171,7 +181,7 @@ export function logToolInvocation(entry, options) {
171
181
  }
172
182
  mkdirSync(logsDir, { recursive: true });
173
183
  const logFilePath = join(logsDir, "invocation.log");
174
- appendFileSync(logFilePath, JSON.stringify(entry) + "\n", "utf8");
184
+ appendBoundedTextFileSync(logFilePath, JSON.stringify(entry) + "\n", MAX_INVOCATION_LOG_BYTES);
175
185
  }
176
186
  catch (err) {
177
187
  // 日志失败不应阻断主流程,仅向 stderr 记录错误
@@ -276,10 +276,13 @@ function pathMatchesPattern(relPath, pattern) {
276
276
  relPath.toLowerCase().startsWith(`${normalizedPattern.toLowerCase()}/`);
277
277
  }
278
278
  function parseGithubRepo(repository) {
279
+ const repositoryObject = repository && typeof repository === "object" && !Array.isArray(repository)
280
+ ? repository
281
+ : null;
279
282
  const raw = typeof repository === "string"
280
283
  ? repository
281
- : repository && typeof repository === "object" && typeof repository.url === "string"
282
- ? repository.url
284
+ : typeof repositoryObject?.url === "string"
285
+ ? repositoryObject.url
283
286
  : "";
284
287
  const match = raw.match(/github\.com[:/](.+?\/.+?)(?:\.git)?(?:[#?].*)?$/i);
285
288
  return match ? match[1] : null;
@@ -30,7 +30,7 @@ export interface PackedCheckResult extends StageCheckResult {
30
30
  }
31
31
  export interface HttpResponse {
32
32
  statusCode: number;
33
- data: any;
33
+ data: unknown;
34
34
  }
35
35
  export type HttpGetFn = (url: string, headers?: Record<string, string>, timeoutMs?: number) => Promise<HttpResponse>;
36
36
  export interface ReleaseGateOptions {
@@ -14,11 +14,13 @@
14
14
  * Network errors resolve to "not_checked" so transient issues don't block.
15
15
  * GITHUB_TOKEN is used as Bearer auth but never logged or returned.
16
16
  */
17
- import { execSync } from "node:child_process";
18
17
  import { createHash } from "node:crypto";
19
- import { readFileSync, writeFileSync } from "node:fs";
18
+ import { readFileSync } from "node:fs";
20
19
  import { join } from "node:path";
21
20
  import { get } from "node:https";
21
+ import { resolveTrustedCommandLine } from "../runner/processSecurity.js";
22
+ import { runSimpleProcessSync } from "../runner/simpleProcess.js";
23
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
22
24
  // ── Constants ──────────────────────────────────────────────────────
23
25
  const STAGE_ORDER = [
24
26
  "local_ready",
@@ -29,6 +31,7 @@ const STAGE_ORDER = [
29
31
  ];
30
32
  const LOCAL_TIMEOUT_MS = 300000;
31
33
  const REMOTE_TIMEOUT_MS = 10000;
34
+ const MAX_REMOTE_RESPONSE_BYTES = 5 * 1024 * 1024;
32
35
  const USER_AGENT = "PatchWarden-ReleaseGate";
33
36
  const PACK_FORBIDDEN_PATTERNS = [
34
37
  /(^|[\\/])\.env$/,
@@ -55,6 +58,14 @@ export function computeTokenDigest(token) {
55
58
  export async function httpsGetJson(url, headers, timeoutMs) {
56
59
  const timeout = timeoutMs ?? REMOTE_TIMEOUT_MS;
57
60
  return new Promise((resolve, reject) => {
61
+ let settled = false;
62
+ const fail = (error) => {
63
+ if (settled)
64
+ return;
65
+ settled = true;
66
+ clearTimeout(timer);
67
+ reject(error);
68
+ };
58
69
  const timer = setTimeout(() => {
59
70
  req.destroy(new Error(`Request timed out after ${timeout}ms`));
60
71
  }, timeout);
@@ -65,9 +76,31 @@ export async function httpsGetJson(url, headers, timeoutMs) {
65
76
  ...(headers || {}),
66
77
  },
67
78
  }, (res) => {
79
+ const declaredLength = Number(res.headers["content-length"]);
80
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_RESPONSE_BYTES) {
81
+ res.destroy();
82
+ fail(new Error(`Response exceeds ${MAX_REMOTE_RESPONSE_BYTES} bytes`));
83
+ return;
84
+ }
68
85
  const chunks = [];
69
- res.on("data", (chunk) => chunks.push(chunk));
86
+ let totalBytes = 0;
87
+ let oversized = false;
88
+ res.on("data", (chunk) => {
89
+ if (oversized)
90
+ return;
91
+ totalBytes += chunk.length;
92
+ if (totalBytes > MAX_REMOTE_RESPONSE_BYTES) {
93
+ oversized = true;
94
+ res.destroy();
95
+ fail(new Error(`Response exceeds ${MAX_REMOTE_RESPONSE_BYTES} bytes`));
96
+ return;
97
+ }
98
+ chunks.push(chunk);
99
+ });
70
100
  res.on("end", () => {
101
+ if (oversized || settled)
102
+ return;
103
+ settled = true;
71
104
  clearTimeout(timer);
72
105
  const body = Buffer.concat(chunks).toString("utf-8");
73
106
  let data = null;
@@ -81,11 +114,10 @@ export async function httpsGetJson(url, headers, timeoutMs) {
81
114
  }
82
115
  resolve({ statusCode: res.statusCode ?? 0, data });
83
116
  });
117
+ res.on("aborted", () => fail(new Error("Response was aborted before completion")));
118
+ res.on("error", fail);
84
119
  });
85
- req.on("error", (err) => {
86
- clearTimeout(timer);
87
- reject(err);
88
- });
120
+ req.on("error", fail);
89
121
  });
90
122
  }
91
123
  /**
@@ -138,12 +170,14 @@ export function checkLocalReady(repoPath) {
138
170
  const commands = ["npm.cmd run build", "npm.cmd test", "npm.cmd run doctor:ci"];
139
171
  for (const cmdStr of commands) {
140
172
  try {
141
- execSync(cmdStr, {
142
- cwd: repoPath,
143
- encoding: "utf-8",
144
- timeout: LOCAL_TIMEOUT_MS,
145
- stdio: ["ignore", "pipe", "pipe"],
146
- });
173
+ const result = runLocalCommand(repoPath, cmdStr, LOCAL_TIMEOUT_MS);
174
+ if (result.spawnError)
175
+ throw new Error(result.spawnError);
176
+ if (result.timedOut)
177
+ throw new Error(`Command timed out after ${LOCAL_TIMEOUT_MS}ms`);
178
+ if (result.exitCode !== 0) {
179
+ throw Object.assign(new Error(`Command exited with code ${result.exitCode ?? -1}`), { status: result.exitCode ?? -1 });
180
+ }
147
181
  }
148
182
  catch (err) {
149
183
  if (err && typeof err === "object" && "status" in err) {
@@ -170,12 +204,14 @@ export function checkLocalReady(repoPath) {
170
204
  export function checkPackedReady(repoPath) {
171
205
  let packOutput;
172
206
  try {
173
- packOutput = execSync("npm.cmd pack --dry-run --json", {
174
- cwd: repoPath,
175
- encoding: "utf-8",
176
- timeout: LOCAL_TIMEOUT_MS,
177
- stdio: ["ignore", "pipe", "pipe"],
178
- });
207
+ const result = runLocalCommand(repoPath, "npm.cmd pack --dry-run --json", LOCAL_TIMEOUT_MS, 10 * 1024 * 1024);
208
+ if (result.spawnError)
209
+ throw new Error(result.spawnError);
210
+ if (result.timedOut)
211
+ throw new Error(`npm pack timed out after ${LOCAL_TIMEOUT_MS}ms`);
212
+ if (result.exitCode !== 0)
213
+ throw new Error(`npm pack exited with code ${result.exitCode ?? -1}`);
214
+ packOutput = result.stdout;
179
215
  }
180
216
  catch (err) {
181
217
  return {
@@ -240,7 +276,7 @@ export function checkPackedReady(repoPath) {
240
276
  };
241
277
  const manifestPath = join(repoPath, "release-artifact-manifest.json");
242
278
  try {
243
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
279
+ atomicWriteJsonFileSync(manifestPath, manifest);
244
280
  }
245
281
  catch (err) {
246
282
  return {
@@ -264,6 +300,17 @@ export function checkPackedReady(repoPath) {
264
300
  }
265
301
  return { status: "passed", manifestPath };
266
302
  }
303
+ function runLocalCommand(repoPath, commandLine, timeoutMs, maxStdoutBytes = 512 * 1024) {
304
+ const invocation = resolveTrustedCommandLine(commandLine, repoPath);
305
+ return runSimpleProcessSync({
306
+ command: invocation.command,
307
+ args: invocation.argsPrefix,
308
+ cwd: repoPath,
309
+ timeoutMs,
310
+ maxStdoutBytes,
311
+ maxStderrBytes: 512 * 1024,
312
+ });
313
+ }
267
314
  /**
268
315
  * Stage 3: published_verified
269
316
  * Queries the npm registry and confirms the given version exists.
@@ -280,11 +327,12 @@ export async function checkPublishedVerified(packageName, version, httpGet) {
280
327
  if (res.statusCode === 404) {
281
328
  return { status: "failed", reason: `Package "${packageName}" not found on npm registry` };
282
329
  }
283
- const data = res.data;
284
- if (!data || !data.versions || typeof data.versions !== "object") {
330
+ const data = asRecord(res.data);
331
+ const versions = asRecord(data?.versions);
332
+ if (!versions) {
285
333
  return { status: "failed", reason: "npm registry response missing versions object" };
286
334
  }
287
- if (Object.prototype.hasOwnProperty.call(data.versions, version)) {
335
+ if (Object.prototype.hasOwnProperty.call(versions, version)) {
288
336
  return { status: "passed" };
289
337
  }
290
338
  return {
@@ -340,12 +388,12 @@ export async function checkCiVerified(repo, branch, httpGet) {
340
388
  if (res.statusCode !== 200) {
341
389
  return { status: "not_checked", reason: `GitHub API returned ${res.statusCode}` };
342
390
  }
343
- const data = res.data;
391
+ const data = asRecord(res.data);
344
392
  const runs = Array.isArray(data?.workflow_runs) ? data.workflow_runs : [];
345
393
  if (runs.length === 0) {
346
394
  return { status: "not_checked", reason: `No CI runs found for branch "${branch}"` };
347
395
  }
348
- const conclusion = runs[0]?.conclusion;
396
+ const conclusion = asRecord(runs[0])?.conclusion;
349
397
  if (conclusion === "success") {
350
398
  return { status: "passed" };
351
399
  }
@@ -364,6 +412,11 @@ export async function checkCiVerified(repo, branch, httpGet) {
364
412
  };
365
413
  }
366
414
  }
415
+ function asRecord(value) {
416
+ return value && typeof value === "object" && !Array.isArray(value)
417
+ ? value
418
+ : null;
419
+ }
367
420
  // ── Orchestrator ───────────────────────────────────────────────────
368
421
  /**
369
422
  * Run release-gate stages up to and including targetStage, in order.
@@ -6,7 +6,21 @@ export interface AgentInvocation {
6
6
  commandLabel: string;
7
7
  promptMode: "inline" | "file";
8
8
  promptFilePath?: string;
9
+ environmentVariableNames: string[];
10
+ blockedEnvironmentVariableNames: string[];
9
11
  }
12
+ export interface ResolvedAgentLaunch {
13
+ command: string;
14
+ argsPrefix: string[];
15
+ }
16
+ /**
17
+ * Resolve the Windows npm shim used by OpenCode to its native executable.
18
+ * Node cannot spawn .cmd/.ps1 shims directly with shell disabled (EINVAL), and
19
+ * enabling a shell would make the task prompt part of shell parsing.
20
+ */
21
+ export declare function resolveAgentExecutable(agentName: string, command: string, platform?: NodeJS.Platform, pathValue?: string, fileExists?: (path: string) => boolean, cwd?: string): string;
22
+ /** Resolve a configured Agent to a native executable or verified npm CLI. */
23
+ export declare function resolveAgentLaunch(agentName: string, command: string, platform?: NodeJS.Platform, pathValue?: string, fileExists?: (path: string) => boolean, cwd?: string, adapterName?: string, readText?: (path: string) => string): ResolvedAgentLaunch;
10
24
  /**
11
25
  * Build agent invocation parameters from config.
12
26
  * Replaces {repo}, {prompt}, and {prompt_file} placeholders.