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
package/src/httpServer.ts CHANGED
@@ -16,52 +16,68 @@
16
16
  */
17
17
 
18
18
  import { createServer, IncomingMessage, ServerResponse } from "node:http";
19
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
20
20
  import { join, resolve } from "node:path";
21
21
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
22
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
23
  import { loadConfig, getTasksDir } from "./config.js";
24
24
  import { registerTools } from "./tools/registry.js";
25
- import { healthCheck } from "./tools/healthCheck.js";
25
+ import { healthCheck } from "./tools/diagnostics/healthCheck.js";
26
26
  import { getToolCatalogSnapshot } from "./tools/registry.js";
27
27
  import { PATCHWARDEN_VERSION } from "./version.js";
28
+ import { logger } from "./logging.js";
29
+ import { firstHeaderValue, timingSafeStringEqual } from "./security/secretComparison.js";
30
+ import { redactSensitiveContent } from "./security/contentRedaction.js";
31
+ import { atomicWriteJsonFileSync } from "./utils/atomicFile.js";
32
+ import { mutateTaskStatus } from "./runner/taskStatusStore.js";
33
+ import { isTrustedLoopbackHostHeader } from "./security/loopbackHost.js";
28
34
 
29
35
  // ── Bootstrap ─────────────────────────────────────────────────────
30
36
 
31
37
  const config = loadConfig();
32
38
  const port = parseInt(process.env.PATCHWARDEN_HTTP_PORT || "") ||
33
- (config as any).httpPort ||
39
+ config.httpPort ||
34
40
  7331;
35
41
  const host = "127.0.0.1";
36
42
 
37
- console.error(`[patchwarden-http] Workspace: ${config.workspaceRoot}`);
38
- console.error(`[patchwarden-http] Listening: http://${host}:${port}/mcp`);
39
- console.error(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed to network`);
43
+ logger.info(`[patchwarden-http] Workspace: ${config.workspaceRoot}`);
44
+ logger.info(`[patchwarden-http] Listening: http://${host}:${port}/mcp`);
45
+ logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed to network`);
40
46
 
41
47
  // ── Owner token (optional) ────────────────────────────────────────
42
48
 
43
- const httpCfg = (config as any).http || {};
49
+ const httpCfg = config.http || {};
44
50
  const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
45
51
  const ownerToken = process.env[ownerTokenEnv] || "";
52
+ const MAX_ADMIN_BODY_BYTES = 64 * 1024;
53
+ const MAX_ACCEPTANCE_NOTES_CHARS = 10_000;
54
+ const REVIEWABLE_TASK_STATUSES = new Set([
55
+ "done_by_agent",
56
+ "done",
57
+ "accepted",
58
+ "rejected",
59
+ "needs_fix",
60
+ "blocked",
61
+ ]);
46
62
 
47
63
  if (ownerToken) {
48
- console.error(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
64
+ logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
49
65
  } else {
50
- console.error(`[patchwarden-http] ⚠️ No owner token set — all local requests accepted`);
66
+ logger.info(`[patchwarden-http] ⚠️ No owner token set — all local requests accepted`);
51
67
  }
52
68
 
53
69
  function checkOwnerToken(req: IncomingMessage): boolean {
54
70
  if (!ownerToken) return true; // no token configured — allow all
55
71
 
56
- const authHeader = req.headers["authorization"] || "";
57
- const customHeader = req.headers["x-patchwarden-token"] || "";
72
+ const authHeader = firstHeaderValue(req.headers["authorization"]);
73
+ const customHeader = firstHeaderValue(req.headers["x-patchwarden-token"]);
58
74
 
59
75
  if (authHeader.startsWith("Bearer ")) {
60
- return authHeader.slice(7) === ownerToken;
76
+ return timingSafeStringEqual(authHeader.slice(7), ownerToken);
61
77
  }
62
78
 
63
- if (typeof customHeader === "string" && customHeader.length > 0) {
64
- return customHeader === ownerToken;
79
+ if (customHeader.length > 0) {
80
+ return timingSafeStringEqual(customHeader, ownerToken);
65
81
  }
66
82
 
67
83
  return false;
@@ -81,11 +97,22 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
81
97
  throw Object.assign(new Error(`Task "${taskId}" not found.`), { statusCode: 404 });
82
98
  }
83
99
  let notes = "";
84
- try {
85
- const parsed = JSON.parse(body || "{}");
86
- notes = String(parsed.notes || parsed.reason || "");
87
- } catch {
88
- // no body or invalid JSON — notes stays empty
100
+ if (body.trim()) {
101
+ let parsed: unknown;
102
+ try {
103
+ parsed = JSON.parse(body);
104
+ } catch {
105
+ throw httpError(400, "Acceptance body must be valid JSON.");
106
+ }
107
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
108
+ throw httpError(400, "Acceptance body must be a JSON object.");
109
+ }
110
+ const record = parsed as Record<string, unknown>;
111
+ const rawNotes = record.notes ?? record.reason ?? "";
112
+ if (typeof rawNotes !== "string") {
113
+ throw httpError(400, "Acceptance notes or reason must be a string.");
114
+ }
115
+ notes = redactSensitiveContent(rawNotes).content.slice(0, MAX_ACCEPTANCE_NOTES_CHARS);
89
116
  }
90
117
  const acceptance = {
91
118
  status,
@@ -93,23 +120,69 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
93
120
  reviewer: "human",
94
121
  notes,
95
122
  };
96
- writeFileSync(filePath, JSON.stringify(acceptance, null, 2), "utf-8");
97
-
98
- // Also update the task's status.json with acceptance_status
123
+ // Commit the review artifact and status annotation while holding the same
124
+ // status lock used by the runner, cancel, and audit paths.
99
125
  const statusFile = join(taskDir, "status.json");
100
- if (existsSync(statusFile)) {
101
- try {
102
- const current = JSON.parse(readFileSync(statusFile, "utf-8"));
103
- current.acceptance_status = status;
104
- current.acceptance_reviewed_at = acceptance.reviewed_at;
105
- writeFileSync(statusFile, JSON.stringify(current, null, 2), "utf-8");
106
- } catch {
107
- // status.json update is best-effort
108
- }
126
+ if (!existsSync(statusFile)) {
127
+ throw httpError(409, `Task "${taskId}" has no status.json to review.`);
109
128
  }
129
+ mutateTaskStatus(statusFile, (current) => {
130
+ if (!REVIEWABLE_TASK_STATUSES.has(String(current.status || ""))) {
131
+ throw httpError(409, `Task "${taskId}" is not in a reviewable terminal state.`);
132
+ }
133
+ atomicWriteJsonFileSync(filePath, acceptance);
134
+ const next = {
135
+ ...current,
136
+ acceptance_status: status,
137
+ acceptance_reviewed_at: acceptance.reviewed_at,
138
+ updated_at: new Date().toISOString(),
139
+ };
140
+ return { next, result: next };
141
+ });
110
142
  return acceptance;
111
143
  }
112
144
 
145
+ function httpError(statusCode: number, message: string): Error & { statusCode: number } {
146
+ return Object.assign(new Error(message), { statusCode });
147
+ }
148
+
149
+ function readBoundedRequestBody(req: IncomingMessage, maxBytes: number): Promise<string> {
150
+ const declaredLength = Number(firstHeaderValue(req.headers["content-length"]));
151
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
152
+ req.resume();
153
+ return Promise.reject(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
154
+ }
155
+ return new Promise((resolveBody, rejectBody) => {
156
+ const chunks: Buffer[] = [];
157
+ let total = 0;
158
+ let settled = false;
159
+ req.on("data", (chunk: Buffer | string) => {
160
+ if (settled) return;
161
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
162
+ total += buffer.length;
163
+ if (total > maxBytes) {
164
+ settled = true;
165
+ req.resume();
166
+ rejectBody(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
167
+ return;
168
+ }
169
+ chunks.push(buffer);
170
+ });
171
+ req.on("end", () => {
172
+ if (!settled) {
173
+ settled = true;
174
+ resolveBody(Buffer.concat(chunks).toString("utf-8"));
175
+ }
176
+ });
177
+ req.on("error", (error) => {
178
+ if (!settled) {
179
+ settled = true;
180
+ rejectBody(error);
181
+ }
182
+ });
183
+ });
184
+ }
185
+
113
186
  function readAcceptance(taskId: string): object {
114
187
  const filePath = getAcceptancePath(taskId);
115
188
  if (!existsSync(filePath)) {
@@ -142,7 +215,9 @@ async function handleMcpRequest(req: IncomingMessage, res: ServerResponse): Prom
142
215
  await mcpServer.connect(transport);
143
216
  await transport.handleRequest(req, res);
144
217
  } catch (err) {
145
- console.error("[patchwarden-http] Request error:", err);
218
+ logger.error("[patchwarden-http] Request error", {
219
+ error: err instanceof Error ? err.message : String(err),
220
+ });
146
221
  if (!res.headersSent) {
147
222
  res.writeHead(500, { "Content-Type": "application/json" });
148
223
  res.end(JSON.stringify({ error: "Internal server error" }));
@@ -178,6 +253,16 @@ function parseAdminUrl(url: string) {
178
253
  // ── HTTP server ───────────────────────────────────────────────────
179
254
 
180
255
  const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
256
+ if (!isTrustedLoopbackHostHeader(req.headers.host, port)) {
257
+ res.writeHead(421, {
258
+ "Content-Type": "application/json; charset=utf-8",
259
+ "Cache-Control": "no-store",
260
+ "X-Content-Type-Options": "nosniff",
261
+ });
262
+ res.end(JSON.stringify({ error: "Untrusted Host header" }));
263
+ return;
264
+ }
265
+
181
266
  // Health check endpoints
182
267
  if (req.method === "GET" && (req.url === "/healthz" || req.url === "/readyz")) {
183
268
  const health = healthCheck(getToolCatalogSnapshot());
@@ -203,9 +288,7 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
203
288
  return;
204
289
  }
205
290
  if ((admin.action === "accept" || admin.action === "reject") && req.method === "POST") {
206
- const chunks: Buffer[] = [];
207
- for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }
208
- const body = Buffer.concat(chunks).toString("utf-8");
291
+ const body = await readBoundedRequestBody(req, MAX_ADMIN_BODY_BYTES);
209
292
  const acceptance = handleAcceptance(admin.taskId, admin.action === "accept" ? "accepted" : "rejected", body);
210
293
  res.writeHead(200, { "Content-Type": "application/json" });
211
294
  res.end(JSON.stringify(acceptance, null, 2));
@@ -213,10 +296,13 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
213
296
  }
214
297
  res.writeHead(405, { "Content-Type": "application/json" });
215
298
  res.end(JSON.stringify({ error: "Method not allowed for this admin endpoint." }));
216
- } catch (err: any) {
217
- const statusCode = err.statusCode || 500;
299
+ } catch (err: unknown) {
300
+ const statusCode = err && typeof err === "object" && "statusCode" in err
301
+ ? Number((err as { statusCode?: unknown }).statusCode) || 500
302
+ : 500;
303
+ const message = err instanceof Error ? err.message : "Internal server error";
218
304
  res.writeHead(statusCode, { "Content-Type": "application/json" });
219
- res.end(JSON.stringify({ error: err.message || "Internal server error" }));
305
+ res.end(JSON.stringify({ error: message }));
220
306
  }
221
307
  return;
222
308
  }
@@ -253,22 +339,22 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
253
339
 
254
340
  httpServer.on("error", (err: NodeJS.ErrnoException) => {
255
341
  if (err.code === "EADDRINUSE") {
256
- console.error(`[patchwarden-http] Fatal: port ${port} is already in use on ${host}.`);
257
- console.error("[patchwarden-http] Stop the other PatchWarden HTTP instance or change httpPort in patchwarden.config.json.");
342
+ logger.fatal(`[patchwarden-http] Fatal: port ${port} is already in use on ${host}.`);
343
+ logger.fatal("[patchwarden-http] Stop the other PatchWarden HTTP instance or change httpPort in patchwarden.config.json.");
258
344
  } else {
259
- console.error(`[patchwarden-http] Fatal: ${err.message}`);
345
+ logger.fatal(`[patchwarden-http] Fatal: ${err.message}`);
260
346
  }
261
347
  process.exit(1);
262
348
  });
263
349
 
264
350
  httpServer.listen(port, host, () => {
265
- console.error(`[patchwarden-http] ✅ Ready`);
266
- console.error(`[patchwarden-http] Admin: http://${host}:${port}/admin/tasks/:id/accept`);
351
+ logger.info(`[patchwarden-http] ✅ Ready`);
352
+ logger.info(`[patchwarden-http] Admin: http://${host}:${port}/admin/tasks/:id/accept`);
267
353
  });
268
354
 
269
355
  // Graceful shutdown
270
356
  process.on("SIGINT", () => {
271
- console.error("[patchwarden-http] Shutting down...");
357
+ logger.info("[patchwarden-http] Shutting down...");
272
358
  httpServer.close(() => process.exit(0));
273
359
  });
274
360
 
package/src/index.ts CHANGED
@@ -12,11 +12,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
12
12
  import { loadConfig } from "./config.js";
13
13
  import { registerTools } from "./tools/registry.js";
14
14
  import { PATCHWARDEN_VERSION } from "./version.js";
15
+ import { logger } from "./logging.js";
15
16
 
16
17
  const config = loadConfig();
17
18
 
18
- console.error(`[patchwarden] Workspace: ${config.workspaceRoot}`);
19
- console.error(`[patchwarden] Transport: stdio`);
19
+ logger.info(`[patchwarden] Workspace: ${config.workspaceRoot}`);
20
+ logger.info("[patchwarden] Transport: stdio");
20
21
 
21
22
  const server = new Server(
22
23
  { name: "patchwarden", version: PATCHWARDEN_VERSION },
@@ -27,8 +28,10 @@ registerTools(server);
27
28
 
28
29
  const transport = new StdioServerTransport();
29
30
  server.connect(transport).catch((err) => {
30
- console.error("[patchwarden] Fatal:", err);
31
+ logger.fatal("[patchwarden] Fatal", {
32
+ error: err instanceof Error ? err.message : String(err),
33
+ });
31
34
  process.exit(1);
32
35
  });
33
36
 
34
- console.error("[patchwarden] MCP server ready on stdio");
37
+ logger.info("[patchwarden] MCP server ready on stdio");
package/src/logging.ts CHANGED
@@ -1,13 +1,16 @@
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
+
9
+ const MAX_INVOCATION_LOG_BYTES = 5 * 1024 * 1024;
7
10
 
8
11
  // ── Types ─────────────────────────────────────────────────────────
9
12
 
10
- export type LogLevel = "info" | "warn" | "error" | "audit";
13
+ export type LogLevel = "info" | "warn" | "error" | "fatal" | "audit";
11
14
 
12
15
  export interface LogEntry {
13
16
  timestamp: string;
@@ -90,6 +93,15 @@ export class Logger {
90
93
  emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
91
94
  }
92
95
 
96
+ /**
97
+ * Emit a fatal-level log entry. Use this immediately before a process
98
+ * exits with a non-zero status, to record the unrecoverable condition.
99
+ * Level is higher than `error`.
100
+ */
101
+ fatal(message: string, context?: Record<string, unknown>): void {
102
+ emit({ timestamp: new Date().toISOString(), level: "fatal", message, ...context });
103
+ }
104
+
93
105
  /**
94
106
  * Emit a tool-call audit log entry.
95
107
  *
@@ -225,7 +237,11 @@ export function logToolInvocation(
225
237
 
226
238
  mkdirSync(logsDir, { recursive: true });
227
239
  const logFilePath = join(logsDir, "invocation.log");
228
- appendFileSync(logFilePath, JSON.stringify(entry) + "\n", "utf8");
240
+ appendBoundedTextFileSync(
241
+ logFilePath,
242
+ JSON.stringify(entry) + "\n",
243
+ MAX_INVOCATION_LOG_BYTES,
244
+ );
229
245
  } catch (err) {
230
246
  // 日志失败不应阻断主流程,仅向 stderr 记录错误
231
247
  try {
@@ -334,10 +334,13 @@ function pathMatchesPattern(relPath: string, pattern: string): boolean {
334
334
  }
335
335
 
336
336
  function parseGithubRepo(repository: unknown): string | null {
337
+ const repositoryObject = repository && typeof repository === "object" && !Array.isArray(repository)
338
+ ? repository as Record<string, unknown>
339
+ : null;
337
340
  const raw = typeof repository === "string"
338
341
  ? repository
339
- : repository && typeof repository === "object" && typeof (repository as any).url === "string"
340
- ? (repository as any).url
342
+ : typeof repositoryObject?.url === "string"
343
+ ? repositoryObject.url
341
344
  : "";
342
345
  const match = raw.match(/github\.com[:/](.+?\/.+?)(?:\.git)?(?:[#?].*)?$/i);
343
346
  return match ? match[1] : null;
@@ -15,11 +15,13 @@
15
15
  * GITHUB_TOKEN is used as Bearer auth but never logged or returned.
16
16
  */
17
17
 
18
- import { execSync } from "node:child_process";
19
18
  import { createHash } from "node:crypto";
20
- import { readFileSync, writeFileSync } from "node:fs";
19
+ import { readFileSync } from "node:fs";
21
20
  import { join } from "node:path";
22
21
  import { get } from "node:https";
22
+ import { resolveTrustedCommandLine } from "../runner/processSecurity.js";
23
+ import { runSimpleProcessSync } from "../runner/simpleProcess.js";
24
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
23
25
 
24
26
  // ── Public types ───────────────────────────────────────────────────
25
27
 
@@ -49,7 +51,7 @@ export interface PackedCheckResult extends StageCheckResult {
49
51
 
50
52
  export interface HttpResponse {
51
53
  statusCode: number;
52
- data: any;
54
+ data: unknown;
53
55
  }
54
56
 
55
57
  export type HttpGetFn = (
@@ -86,6 +88,7 @@ const STAGE_ORDER: ReleaseStage[] = [
86
88
 
87
89
  const LOCAL_TIMEOUT_MS = 300000;
88
90
  const REMOTE_TIMEOUT_MS = 10000;
91
+ const MAX_REMOTE_RESPONSE_BYTES = 5 * 1024 * 1024;
89
92
  const USER_AGENT = "PatchWarden-ReleaseGate";
90
93
 
91
94
  const PACK_FORBIDDEN_PATTERNS: RegExp[] = [
@@ -121,6 +124,13 @@ export async function httpsGetJson(
121
124
  ): Promise<HttpResponse> {
122
125
  const timeout = timeoutMs ?? REMOTE_TIMEOUT_MS;
123
126
  return new Promise<HttpResponse>((resolve, reject) => {
127
+ let settled = false;
128
+ const fail = (error: Error) => {
129
+ if (settled) return;
130
+ settled = true;
131
+ clearTimeout(timer);
132
+ reject(error);
133
+ };
124
134
  const timer = setTimeout(() => {
125
135
  req.destroy(new Error(`Request timed out after ${timeout}ms`));
126
136
  }, timeout);
@@ -135,12 +145,32 @@ export async function httpsGetJson(
135
145
  },
136
146
  },
137
147
  (res) => {
148
+ const declaredLength = Number(res.headers["content-length"]);
149
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_RESPONSE_BYTES) {
150
+ res.destroy();
151
+ fail(new Error(`Response exceeds ${MAX_REMOTE_RESPONSE_BYTES} bytes`));
152
+ return;
153
+ }
138
154
  const chunks: Buffer[] = [];
139
- res.on("data", (chunk: Buffer) => chunks.push(chunk));
155
+ let totalBytes = 0;
156
+ let oversized = false;
157
+ res.on("data", (chunk: Buffer) => {
158
+ if (oversized) return;
159
+ totalBytes += chunk.length;
160
+ if (totalBytes > MAX_REMOTE_RESPONSE_BYTES) {
161
+ oversized = true;
162
+ res.destroy();
163
+ fail(new Error(`Response exceeds ${MAX_REMOTE_RESPONSE_BYTES} bytes`));
164
+ return;
165
+ }
166
+ chunks.push(chunk);
167
+ });
140
168
  res.on("end", () => {
169
+ if (oversized || settled) return;
170
+ settled = true;
141
171
  clearTimeout(timer);
142
172
  const body = Buffer.concat(chunks).toString("utf-8");
143
- let data: any = null;
173
+ let data: unknown = null;
144
174
  if (body.length > 0) {
145
175
  try {
146
176
  data = JSON.parse(body);
@@ -150,12 +180,11 @@ export async function httpsGetJson(
150
180
  }
151
181
  resolve({ statusCode: res.statusCode ?? 0, data });
152
182
  });
183
+ res.on("aborted", () => fail(new Error("Response was aborted before completion")));
184
+ res.on("error", fail);
153
185
  },
154
186
  );
155
- req.on("error", (err) => {
156
- clearTimeout(timer);
157
- reject(err);
158
- });
187
+ req.on("error", fail);
159
188
  });
160
189
  }
161
190
 
@@ -203,12 +232,12 @@ export function checkLocalReady(repoPath: string): StageCheckResult {
203
232
  const commands = ["npm.cmd run build", "npm.cmd test", "npm.cmd run doctor:ci"];
204
233
  for (const cmdStr of commands) {
205
234
  try {
206
- execSync(cmdStr, {
207
- cwd: repoPath,
208
- encoding: "utf-8",
209
- timeout: LOCAL_TIMEOUT_MS,
210
- stdio: ["ignore", "pipe", "pipe"],
211
- });
235
+ const result = runLocalCommand(repoPath, cmdStr, LOCAL_TIMEOUT_MS);
236
+ if (result.spawnError) throw new Error(result.spawnError);
237
+ if (result.timedOut) throw new Error(`Command timed out after ${LOCAL_TIMEOUT_MS}ms`);
238
+ if (result.exitCode !== 0) {
239
+ throw Object.assign(new Error(`Command exited with code ${result.exitCode ?? -1}`), { status: result.exitCode ?? -1 });
240
+ }
212
241
  } catch (err) {
213
242
  if (err && typeof err === "object" && "status" in err) {
214
243
  const code = (err as { status: unknown }).status;
@@ -235,12 +264,11 @@ export function checkLocalReady(repoPath: string): StageCheckResult {
235
264
  export function checkPackedReady(repoPath: string): PackedCheckResult {
236
265
  let packOutput: string;
237
266
  try {
238
- packOutput = execSync("npm.cmd pack --dry-run --json", {
239
- cwd: repoPath,
240
- encoding: "utf-8",
241
- timeout: LOCAL_TIMEOUT_MS,
242
- stdio: ["ignore", "pipe", "pipe"],
243
- });
267
+ const result = runLocalCommand(repoPath, "npm.cmd pack --dry-run --json", LOCAL_TIMEOUT_MS, 10 * 1024 * 1024);
268
+ if (result.spawnError) throw new Error(result.spawnError);
269
+ if (result.timedOut) throw new Error(`npm pack timed out after ${LOCAL_TIMEOUT_MS}ms`);
270
+ if (result.exitCode !== 0) throw new Error(`npm pack exited with code ${result.exitCode ?? -1}`);
271
+ packOutput = result.stdout;
244
272
  } catch (err) {
245
273
  return {
246
274
  status: "failed",
@@ -309,7 +337,7 @@ export function checkPackedReady(repoPath: string): PackedCheckResult {
309
337
 
310
338
  const manifestPath = join(repoPath, "release-artifact-manifest.json");
311
339
  try {
312
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
340
+ atomicWriteJsonFileSync(manifestPath, manifest);
313
341
  } catch (err) {
314
342
  return {
315
343
  status: "failed",
@@ -335,6 +363,18 @@ export function checkPackedReady(repoPath: string): PackedCheckResult {
335
363
  return { status: "passed", manifestPath };
336
364
  }
337
365
 
366
+ function runLocalCommand(repoPath: string, commandLine: string, timeoutMs: number, maxStdoutBytes = 512 * 1024) {
367
+ const invocation = resolveTrustedCommandLine(commandLine, repoPath);
368
+ return runSimpleProcessSync({
369
+ command: invocation.command,
370
+ args: invocation.argsPrefix,
371
+ cwd: repoPath,
372
+ timeoutMs,
373
+ maxStdoutBytes,
374
+ maxStderrBytes: 512 * 1024,
375
+ });
376
+ }
377
+
338
378
  /**
339
379
  * Stage 3: published_verified
340
380
  * Queries the npm registry and confirms the given version exists.
@@ -355,11 +395,12 @@ export async function checkPublishedVerified(
355
395
  if (res.statusCode === 404) {
356
396
  return { status: "failed", reason: `Package "${packageName}" not found on npm registry` };
357
397
  }
358
- const data = res.data;
359
- if (!data || !data.versions || typeof data.versions !== "object") {
398
+ const data = asRecord(res.data);
399
+ const versions = asRecord(data?.versions);
400
+ if (!versions) {
360
401
  return { status: "failed", reason: "npm registry response missing versions object" };
361
402
  }
362
- if (Object.prototype.hasOwnProperty.call(data.versions, version)) {
403
+ if (Object.prototype.hasOwnProperty.call(versions, version)) {
363
404
  return { status: "passed" };
364
405
  }
365
406
  return {
@@ -423,12 +464,12 @@ export async function checkCiVerified(
423
464
  if (res.statusCode !== 200) {
424
465
  return { status: "not_checked", reason: `GitHub API returned ${res.statusCode}` };
425
466
  }
426
- const data = res.data;
467
+ const data = asRecord(res.data);
427
468
  const runs = Array.isArray(data?.workflow_runs) ? data.workflow_runs : [];
428
469
  if (runs.length === 0) {
429
470
  return { status: "not_checked", reason: `No CI runs found for branch "${branch}"` };
430
471
  }
431
- const conclusion = runs[0]?.conclusion;
472
+ const conclusion = asRecord(runs[0])?.conclusion;
432
473
  if (conclusion === "success") {
433
474
  return { status: "passed" };
434
475
  }
@@ -447,6 +488,12 @@ export async function checkCiVerified(
447
488
  }
448
489
  }
449
490
 
491
+ function asRecord(value: unknown): Record<string, unknown> | null {
492
+ return value && typeof value === "object" && !Array.isArray(value)
493
+ ? value as Record<string, unknown>
494
+ : null;
495
+ }
496
+
450
497
  // ── Orchestrator ───────────────────────────────────────────────────
451
498
 
452
499
  /**