patchwarden 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (573) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +152 -25
  3. package/README.md +120 -23
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentStore.js +42 -34
  6. package/dist/config.d.ts +15 -0
  7. package/dist/config.js +66 -13
  8. package/dist/control/fileManager.d.ts +2 -0
  9. package/dist/control/fileManager.js +39 -0
  10. package/dist/control/middleware/auth.d.ts +6 -0
  11. package/dist/control/middleware/auth.js +14 -5
  12. package/dist/control/middleware/static.js +16 -3
  13. package/dist/control/routeTable.d.ts +24 -0
  14. package/dist/control/routeTable.js +263 -0
  15. package/dist/control/routes/audit.js +17 -7
  16. package/dist/control/routes/evidence.js +1 -1
  17. package/dist/control/routes/lineage.js +1 -1
  18. package/dist/control/routes/process.d.ts +7 -0
  19. package/dist/control/routes/process.js +329 -42
  20. package/dist/control/routes/sessions.d.ts +9 -1
  21. package/dist/control/routes/sessions.js +84 -21
  22. package/dist/control/routes/status.d.ts +46 -1
  23. package/dist/control/routes/status.js +72 -15
  24. package/dist/control/routes/taskActions.js +24 -60
  25. package/dist/control/routes/tasks.d.ts +4 -0
  26. package/dist/control/routes/tasks.js +61 -62
  27. package/dist/control/routes/workspace.js +50 -23
  28. package/dist/control/runtime.d.ts +3 -2
  29. package/dist/control/runtime.js +63 -30
  30. package/dist/control/server.js +36 -365
  31. package/dist/control/shared.d.ts +25 -1
  32. package/dist/control/shared.js +64 -11
  33. package/dist/direct/directAudit.js +234 -225
  34. package/dist/direct/directGuards.d.ts +2 -1
  35. package/dist/direct/directGuards.js +42 -29
  36. package/dist/direct/directPatch.d.ts +6 -1
  37. package/dist/direct/directPatch.js +55 -8
  38. package/dist/direct/directSessionStore.d.ts +16 -6
  39. package/dist/direct/directSessionStore.js +127 -35
  40. package/dist/direct/directVerification.js +8 -16
  41. package/dist/doctor.js +62 -52
  42. package/dist/goal/acceptanceEngine.d.ts +1 -1
  43. package/dist/goal/goalProgress.js +75 -69
  44. package/dist/goal/goalReport.js +5 -12
  45. package/dist/goal/goalStatus.d.ts +2 -0
  46. package/dist/goal/goalStatus.js +10 -3
  47. package/dist/goal/goalStore.d.ts +9 -2
  48. package/dist/goal/goalStore.js +152 -45
  49. package/dist/goal/handoffExport.js +6 -6
  50. package/dist/goal/specKitImport.js +97 -96
  51. package/dist/goal/subgoalSync.js +11 -9
  52. package/dist/goal/worktreeManager.d.ts +2 -0
  53. package/dist/goal/worktreeManager.js +209 -136
  54. package/dist/httpServer.js +111 -32
  55. package/dist/logging.js +5 -3
  56. package/dist/policy/projectPolicy.js +5 -2
  57. package/dist/release/releaseGate.d.ts +1 -1
  58. package/dist/release/releaseGate.js +78 -25
  59. package/dist/runner/agentInvocation.d.ts +14 -0
  60. package/dist/runner/agentInvocation.js +96 -4
  61. package/dist/runner/changeCapture.d.ts +13 -5
  62. package/dist/runner/changeCapture.js +243 -61
  63. package/dist/runner/postTaskCleanup.js +48 -19
  64. package/dist/runner/processSecurity.d.ts +56 -0
  65. package/dist/runner/processSecurity.js +373 -0
  66. package/dist/runner/runTask.d.ts +1 -1
  67. package/dist/runner/runTask.js +115 -87
  68. package/dist/runner/simpleProcess.d.ts +3 -0
  69. package/dist/runner/simpleProcess.js +50 -36
  70. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  71. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  72. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  73. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  74. package/dist/runner/taskStatusStore.d.ts +11 -0
  75. package/dist/runner/taskStatusStore.js +24 -0
  76. package/dist/runner/watch.d.ts +14 -1
  77. package/dist/runner/watch.js +256 -48
  78. package/dist/security/contentRedaction.js +2 -2
  79. package/dist/security/discoveryTokenStore.d.ts +3 -2
  80. package/dist/security/discoveryTokenStore.js +17 -2
  81. package/dist/security/loopbackHost.d.ts +2 -0
  82. package/dist/security/loopbackHost.js +16 -0
  83. package/dist/security/pathGuard.js +32 -17
  84. package/dist/security/planGuard.js +2 -1
  85. package/dist/security/runtimeGuard.js +9 -6
  86. package/dist/security/secretComparison.d.ts +3 -0
  87. package/dist/security/secretComparison.js +11 -0
  88. package/dist/security/sensitiveGuard.d.ts +5 -0
  89. package/dist/security/sensitiveGuard.js +27 -8
  90. package/dist/security/toolInvocationGuard.d.ts +2 -2
  91. package/dist/security/toolInvocationGuard.js +1 -1
  92. package/dist/security/workspaceRootGuard.d.ts +8 -0
  93. package/dist/security/workspaceRootGuard.js +47 -0
  94. package/dist/smoke-test.js +24 -23
  95. package/dist/tools/catalog/index.d.ts +4 -0
  96. package/dist/tools/catalog/index.js +4 -0
  97. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  98. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  99. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  100. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  101. package/dist/tools/definitions/toolDefs.js +1177 -0
  102. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  103. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  104. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
  105. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
  106. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  107. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
  108. package/dist/tools/diagnostics/index.d.ts +6 -0
  109. package/dist/tools/diagnostics/index.js +6 -0
  110. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  111. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  112. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  113. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  114. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  115. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  116. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  117. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  118. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  119. package/dist/tools/direct/index.d.ts +3 -0
  120. package/dist/tools/direct/index.js +3 -0
  121. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  122. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  123. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  124. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  125. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  126. package/dist/tools/discovery/index.d.ts +3 -0
  127. package/dist/tools/discovery/index.js +3 -0
  128. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  129. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  130. package/dist/tools/dispatch/coreDispatch.js +32 -31
  131. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  132. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  133. package/dist/tools/dispatch/directDispatch.js +11 -10
  134. package/dist/tools/dispatch/goalDispatch.js +5 -4
  135. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  136. package/dist/tools/dispatch/validation.d.ts +7 -0
  137. package/dist/tools/dispatch/validation.js +71 -0
  138. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  139. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  140. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  141. package/dist/tools/goals/index.d.ts +3 -0
  142. package/dist/tools/goals/index.js +3 -0
  143. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  144. package/dist/tools/registry.d.ts +7 -11
  145. package/dist/tools/registry.js +17 -1178
  146. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  147. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  148. package/dist/tools/release/index.d.ts +2 -0
  149. package/dist/tools/release/index.js +2 -0
  150. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  151. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  152. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  153. package/dist/tools/tasks/cancelTask.js +85 -0
  154. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
  155. package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
  156. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  157. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  158. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  159. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  160. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  161. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  162. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  163. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  164. package/dist/tools/tasks/index.d.ts +18 -0
  165. package/dist/tools/tasks/index.js +18 -0
  166. package/dist/tools/tasks/killTask.d.ts +1 -0
  167. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  168. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  169. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  170. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  171. package/dist/tools/tasks/retryTask.js +60 -0
  172. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  173. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
  174. package/dist/tools/tasks/runVerification.js +38 -0
  175. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  176. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  177. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  178. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  179. package/dist/tools/workspace/applyPatch.js +53 -0
  180. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  181. package/dist/tools/workspace/index.d.ts +10 -0
  182. package/dist/tools/workspace/index.js +10 -0
  183. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  184. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  185. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  186. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  187. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  188. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  189. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  190. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  191. package/dist/tools/workspace/syncFile.js +134 -0
  192. package/dist/utils/atomicFile.d.ts +7 -0
  193. package/dist/utils/atomicFile.js +48 -0
  194. package/dist/utils/boundedFile.d.ts +8 -0
  195. package/dist/utils/boundedFile.js +93 -0
  196. package/dist/utils/lockedJsonFile.d.ts +15 -0
  197. package/dist/utils/lockedJsonFile.js +224 -0
  198. package/dist/utils/platform.d.ts +11 -0
  199. package/dist/utils/platform.js +26 -0
  200. package/dist/utils/stableJson.d.ts +2 -0
  201. package/dist/utils/stableJson.js +12 -0
  202. package/dist/version.d.ts +3 -2
  203. package/dist/version.js +3 -2
  204. package/dist/watcherStatus.d.ts +2 -2
  205. package/dist/watcherStatus.js +18 -0
  206. package/docs/CODE_WIKI.md +808 -143
  207. package/docs/architecture.md +15 -55
  208. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  209. package/docs/control-center/README.md +4 -4
  210. package/docs/dashboard-overview.md +4 -4
  211. package/docs/desktop-app.md +175 -0
  212. package/docs/evidence-pack-schema.md +3 -3
  213. package/docs/open-source-application.md +17 -11
  214. package/docs/release-checklist.md +1 -1
  215. package/docs/release-evidence.md +39 -29
  216. package/docs/task-safe-review-workflow.md +5 -5
  217. package/docs/threat-model.md +19 -1
  218. package/examples/config.example.json +4 -2
  219. package/package.json +27 -10
  220. package/scripts/README.md +15 -6
  221. package/scripts/build.js +11 -0
  222. package/scripts/checks/brand-check.js +3 -0
  223. package/scripts/checks/build-output-check.js +37 -0
  224. package/scripts/checks/control-center-smoke.js +91 -6
  225. package/scripts/checks/control-smoke.js +19 -8
  226. package/scripts/checks/http-mcp-smoke.js +82 -4
  227. package/scripts/checks/lifecycle-smoke.js +10 -10
  228. package/scripts/checks/mcp-manifest-check.js +1 -1
  229. package/scripts/checks/mcp-smoke.js +27 -3
  230. package/scripts/checks/package-manifest-check.js +20 -7
  231. package/scripts/checks/unit-tests.js +3 -5
  232. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  233. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  234. package/scripts/control/manage-patchwarden.ps1 +69 -4
  235. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  236. package/scripts/control/run-background-supervisor.ps1 +41 -0
  237. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  238. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  239. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  240. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  241. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  242. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  243. package/scripts/lib/clean-generated-output.js +11 -0
  244. package/scripts/lib/file-discovery.js +12 -0
  245. package/scripts/release/desktop-preflight.js +211 -0
  246. package/scripts/release/pack-clean.js +59 -14
  247. package/src/assessments/agentAssessor.ts +26 -12
  248. package/src/assessments/assessmentStore.ts +52 -40
  249. package/src/config.ts +70 -13
  250. package/src/control/fileManager.ts +40 -0
  251. package/src/control/middleware/auth.ts +22 -4
  252. package/src/control/middleware/static.ts +16 -3
  253. package/src/control/routeTable.ts +301 -0
  254. package/src/control/routes/audit.ts +14 -6
  255. package/src/control/routes/evidence.ts +1 -1
  256. package/src/control/routes/lineage.ts +1 -1
  257. package/src/control/routes/process.ts +341 -40
  258. package/src/control/routes/sessions.ts +88 -25
  259. package/src/control/routes/status.ts +97 -17
  260. package/src/control/routes/taskActions.ts +24 -58
  261. package/src/control/routes/tasks.ts +66 -58
  262. package/src/control/routes/workspace.ts +56 -25
  263. package/src/control/runtime.ts +69 -33
  264. package/src/control/server.ts +36 -342
  265. package/src/control/shared.ts +77 -11
  266. package/src/direct/directAudit.ts +17 -9
  267. package/src/direct/directGuards.ts +72 -44
  268. package/src/direct/directPatch.ts +95 -8
  269. package/src/direct/directSessionStore.ts +211 -55
  270. package/src/direct/directVerification.ts +9 -23
  271. package/src/doctor.ts +73 -59
  272. package/src/goal/acceptanceEngine.ts +1 -1
  273. package/src/goal/goalProgress.ts +17 -13
  274. package/src/goal/goalReport.ts +5 -13
  275. package/src/goal/goalStatus.ts +19 -3
  276. package/src/goal/goalStore.ts +241 -54
  277. package/src/goal/handoffExport.ts +6 -6
  278. package/src/goal/specKitImport.ts +12 -12
  279. package/src/goal/subgoalSync.ts +11 -8
  280. package/src/goal/worktreeManager.ts +139 -22
  281. package/src/httpServer.ts +113 -30
  282. package/src/logging.ts +10 -3
  283. package/src/policy/projectPolicy.ts +5 -2
  284. package/src/release/releaseGate.ts +74 -27
  285. package/src/runner/agentInvocation.ts +126 -4
  286. package/src/runner/changeCapture.ts +291 -57
  287. package/src/runner/postTaskCleanup.ts +41 -17
  288. package/src/runner/processSecurity.ts +433 -0
  289. package/src/runner/runTask.ts +138 -91
  290. package/src/runner/simpleProcess.ts +66 -31
  291. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  292. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  293. package/src/runner/taskStatusStore.ts +54 -0
  294. package/src/runner/watch.ts +281 -51
  295. package/src/security/contentRedaction.ts +2 -2
  296. package/src/security/discoveryTokenStore.ts +18 -4
  297. package/src/security/loopbackHost.ts +19 -0
  298. package/src/security/pathGuard.ts +35 -16
  299. package/src/security/planGuard.ts +2 -1
  300. package/src/security/runtimeGuard.ts +9 -6
  301. package/src/security/secretComparison.ts +13 -0
  302. package/src/security/sensitiveGuard.ts +27 -8
  303. package/src/security/toolInvocationGuard.ts +2 -2
  304. package/src/security/workspaceRootGuard.ts +53 -0
  305. package/src/smoke-test.ts +25 -24
  306. package/src/tools/catalog/index.ts +4 -0
  307. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  308. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  309. package/src/tools/definitions/toolDefs.ts +1264 -0
  310. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  311. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
  312. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
  313. package/src/tools/diagnostics/index.ts +6 -0
  314. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  315. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  316. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  317. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  318. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  319. package/src/tools/direct/index.ts +3 -0
  320. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  321. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  322. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  323. package/src/tools/discovery/index.ts +3 -0
  324. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  325. package/src/tools/dispatch/coreDispatch.ts +36 -31
  326. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  327. package/src/tools/dispatch/directDispatch.ts +11 -10
  328. package/src/tools/dispatch/goalDispatch.ts +5 -4
  329. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  330. package/src/tools/dispatch/validation.ts +78 -0
  331. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  332. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  333. package/src/tools/goals/index.ts +3 -0
  334. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  335. package/src/tools/registry.ts +17 -1261
  336. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  337. package/src/tools/release/index.ts +2 -0
  338. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  339. package/src/tools/tasks/cancelTask.ts +109 -0
  340. package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
  341. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  342. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  343. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  344. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  345. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  346. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  347. package/src/tools/tasks/index.ts +18 -0
  348. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  349. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  350. package/src/tools/tasks/retryTask.ts +69 -0
  351. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
  352. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  353. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  354. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  355. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  356. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  357. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  358. package/src/tools/workspace/index.ts +10 -0
  359. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  360. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  361. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  362. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  363. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  364. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  365. package/src/tools/workspace/syncFile.ts +292 -0
  366. package/src/utils/atomicFile.ts +58 -0
  367. package/src/utils/boundedFile.ts +116 -0
  368. package/src/utils/lockedJsonFile.ts +251 -0
  369. package/src/utils/platform.ts +31 -0
  370. package/src/utils/stableJson.ts +12 -0
  371. package/src/version.ts +3 -2
  372. package/src/watcherStatus.ts +19 -1
  373. package/ui/desktop-bootstrap.js +11 -0
  374. package/ui/desktop-bridge.js +36 -0
  375. package/ui/desktop.css +160 -0
  376. package/ui/getting-started.js +93 -0
  377. package/ui/i18n.js +484 -0
  378. package/ui/log-parser.js +30 -0
  379. package/ui/pages/audit.html +12 -0
  380. package/ui/pages/dashboard.html +90 -147
  381. package/ui/pages/direct-sessions.html +123 -26
  382. package/ui/pages/getting-started.html +43 -0
  383. package/ui/pages/logs.html +105 -22
  384. package/ui/pages/settings.html +101 -0
  385. package/ui/pages/task-detail.html +12 -0
  386. package/ui/pages/tasks.html +54 -8
  387. package/ui/pages/workspace.html +38 -1
  388. package/ui/settings.js +245 -0
  389. package/PatchWarden-Control.cmd +0 -6
  390. package/Restart-PatchWarden-Control.cmd +0 -6
  391. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  392. package/dist/test/unit/acceptance-engine.test.js +0 -228
  393. package/dist/test/unit/agent-router.test.d.ts +0 -1
  394. package/dist/test/unit/agent-router.test.js +0 -287
  395. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  396. package/dist/test/unit/android-doctor.test.js +0 -118
  397. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  398. package/dist/test/unit/apply-patch.test.js +0 -225
  399. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  400. package/dist/test/unit/audit-checks.test.js +0 -350
  401. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  402. package/dist/test/unit/chinese-path.test.js +0 -91
  403. package/dist/test/unit/command-guard.test.d.ts +0 -1
  404. package/dist/test/unit/command-guard.test.js +0 -160
  405. package/dist/test/unit/create-task.test.d.ts +0 -1
  406. package/dist/test/unit/create-task.test.js +0 -197
  407. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  408. package/dist/test/unit/diagnose-task.test.js +0 -457
  409. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  410. package/dist/test/unit/direct-guards.test.js +0 -328
  411. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  412. package/dist/test/unit/discovery-token-store.test.js +0 -139
  413. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  414. package/dist/test/unit/evidence-pack.test.js +0 -224
  415. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  416. package/dist/test/unit/get-task-status.test.js +0 -174
  417. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  418. package/dist/test/unit/get-task-summary.test.js +0 -146
  419. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  420. package/dist/test/unit/goal-graph.test.js +0 -298
  421. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  422. package/dist/test/unit/goal-progress.test.js +0 -381
  423. package/dist/test/unit/goal-report.test.d.ts +0 -1
  424. package/dist/test/unit/goal-report.test.js +0 -159
  425. package/dist/test/unit/goal-status.test.d.ts +0 -1
  426. package/dist/test/unit/goal-status.test.js +0 -215
  427. package/dist/test/unit/goal-store.test.d.ts +0 -1
  428. package/dist/test/unit/goal-store.test.js +0 -253
  429. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  430. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  431. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  432. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  433. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  434. package/dist/test/unit/handoff-export.test.js +0 -263
  435. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  436. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  437. package/dist/test/unit/logging.test.d.ts +0 -1
  438. package/dist/test/unit/logging.test.js +0 -397
  439. package/dist/test/unit/path-guard.test.d.ts +0 -1
  440. package/dist/test/unit/path-guard.test.js +0 -133
  441. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  442. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  443. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  444. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  445. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  446. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  447. package/dist/test/unit/release-gate.test.d.ts +0 -1
  448. package/dist/test/unit/release-gate.test.js +0 -242
  449. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  450. package/dist/test/unit/run-task-loop.test.js +0 -380
  451. package/dist/test/unit/safe-status.test.d.ts +0 -1
  452. package/dist/test/unit/safe-status.test.js +0 -165
  453. package/dist/test/unit/safe-views.test.d.ts +0 -1
  454. package/dist/test/unit/safe-views.test.js +0 -171
  455. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  456. package/dist/test/unit/schema-drift-check.test.js +0 -176
  457. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  458. package/dist/test/unit/sensitive-guard.test.js +0 -104
  459. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  460. package/dist/test/unit/spec-kit-import.test.js +0 -341
  461. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  462. package/dist/test/unit/subgoal-sync.test.js +0 -183
  463. package/dist/test/unit/sync-file.test.d.ts +0 -1
  464. package/dist/test/unit/sync-file.test.js +0 -154
  465. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  466. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  467. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  468. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  469. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  470. package/dist/test/unit/toolSearch.test.js +0 -571
  471. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  472. package/dist/test/unit/wait-for-task.test.js +0 -144
  473. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  474. package/dist/test/unit/watcher-status.test.js +0 -169
  475. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  476. package/dist/test/unit/worktree-manager.test.js +0 -176
  477. package/dist/tools/applyPatch.js +0 -41
  478. package/dist/tools/cancelTask.d.ts +0 -30
  479. package/dist/tools/cancelTask.js +0 -64
  480. package/dist/tools/finalizeDirectSession.js +0 -84
  481. package/dist/tools/goalSubgoalTask.js +0 -110
  482. package/dist/tools/killTask.d.ts +0 -15
  483. package/dist/tools/retryTask.js +0 -45
  484. package/dist/tools/runVerification.js +0 -36
  485. package/dist/tools/syncFile.js +0 -65
  486. package/docs/control-center/control-center-daily-driver.md +0 -211
  487. package/docs/control-center/control-center-mvp.md +0 -205
  488. package/docs/control-center/control-center-phase2.md +0 -159
  489. package/docs/execution-plan-2026-07-09.md +0 -315
  490. package/docs/release-v0.6.0.md +0 -71
  491. package/docs/release-v0.6.1.md +0 -75
  492. package/docs/release-v0.6.4.md +0 -45
  493. package/docs/roadmap-execution-and-acceptance.md +0 -365
  494. package/src/test/unit/acceptance-engine.test.ts +0 -261
  495. package/src/test/unit/agent-router.test.ts +0 -342
  496. package/src/test/unit/android-doctor.test.ts +0 -158
  497. package/src/test/unit/apply-patch.test.ts +0 -293
  498. package/src/test/unit/audit-checks.test.ts +0 -567
  499. package/src/test/unit/chinese-path.test.ts +0 -106
  500. package/src/test/unit/command-guard.test.ts +0 -221
  501. package/src/test/unit/create-task.test.ts +0 -255
  502. package/src/test/unit/diagnose-task.test.ts +0 -544
  503. package/src/test/unit/direct-guards.test.ts +0 -467
  504. package/src/test/unit/discovery-token-store.test.ts +0 -181
  505. package/src/test/unit/evidence-pack.test.ts +0 -251
  506. package/src/test/unit/get-task-status.test.ts +0 -203
  507. package/src/test/unit/get-task-summary.test.ts +0 -173
  508. package/src/test/unit/goal-graph.test.ts +0 -347
  509. package/src/test/unit/goal-progress.test.ts +0 -538
  510. package/src/test/unit/goal-report.test.ts +0 -189
  511. package/src/test/unit/goal-status.test.ts +0 -270
  512. package/src/test/unit/goal-store.test.ts +0 -318
  513. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  514. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  515. package/src/test/unit/handoff-export.test.ts +0 -295
  516. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  517. package/src/test/unit/logging.test.ts +0 -497
  518. package/src/test/unit/path-guard.test.ts +0 -180
  519. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  520. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  521. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  522. package/src/test/unit/release-gate.test.ts +0 -314
  523. package/src/test/unit/run-task-loop.test.ts +0 -425
  524. package/src/test/unit/safe-status.test.ts +0 -187
  525. package/src/test/unit/safe-views.test.ts +0 -184
  526. package/src/test/unit/schema-drift-check.test.ts +0 -260
  527. package/src/test/unit/sensitive-guard.test.ts +0 -124
  528. package/src/test/unit/spec-kit-import.test.ts +0 -429
  529. package/src/test/unit/subgoal-sync.test.ts +0 -236
  530. package/src/test/unit/sync-file.test.ts +0 -231
  531. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  532. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  533. package/src/test/unit/toolSearch.test.ts +0 -756
  534. package/src/test/unit/wait-for-task.test.ts +0 -176
  535. package/src/test/unit/watcher-status.test.ts +0 -190
  536. package/src/test/unit/worktree-manager.test.ts +0 -247
  537. package/src/tools/cancelTask.ts +0 -70
  538. package/src/tools/retryTask.ts +0 -53
  539. package/src/tools/syncFile.ts +0 -122
  540. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  541. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  542. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  543. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  544. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  545. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  546. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  547. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  548. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  549. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  550. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  551. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  552. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  553. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  554. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  555. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  556. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  557. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  558. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  559. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  560. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  561. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  562. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  563. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  564. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  565. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  566. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  567. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  568. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  569. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  570. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  571. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  572. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  573. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -18,37 +18,53 @@
18
18
  * 所有文件系统函数都接受可选的 workspaceRoot 参数用于测试;
19
19
  * 默认从 getConfig().workspaceRoot 读取。
20
20
  */
21
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, rmSync, } from "node:fs";
22
- import { join } from "node:path";
21
+ import { existsSync, mkdirSync, readFileSync, rmSync, } from "node:fs";
22
+ import { join, normalize } from "node:path";
23
23
  import { execFileSync } from "node:child_process";
24
24
  import { randomBytes } from "node:crypto";
25
25
  import { getConfig } from "../config.js";
26
- import { guardWorkspacePath } from "../security/pathGuard.js";
26
+ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
27
27
  import { guardSensitivePath } from "../security/sensitiveGuard.js";
28
28
  import { PatchWardenError } from "../errors.js";
29
+ import { buildGitEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
30
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
31
+ import { withFileLockSync } from "../utils/lockedJsonFile.js";
29
32
  // ── 常量 ──────────────────────────────────────────────────────────
30
33
  /** worktree 根目录名,放在 workspaceRoot 下。 */
31
34
  export const WorktreeDir = "_workspacetrees";
32
35
  /** 归档目录名(discard 后保留状态),位于 .patchwarden 下,始终为安全路径。 */
33
36
  const WORKTREE_ARCHIVE_DIR = ".patchwarden/worktree-archive";
37
+ const WORKTREE_LOCK_DIR = ".patchwarden/worktree-locks";
34
38
  const GIT_TIMEOUT_MS = 30000;
35
39
  const GIT_BRANCH_TIMEOUT_MS = 15000;
40
+ const WORKTREE_ID_MAX_LENGTH = 32;
41
+ const WORKTREE_ID_PATTERN = /^wt_[a-z0-9]{1,16}_[a-f0-9]{12}$/;
36
42
  // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
37
43
  function resolveWorkspaceRoot(workspaceRoot) {
38
44
  return workspaceRoot ?? getConfig().workspaceRoot;
39
45
  }
46
+ /** Accept only IDs emitted by generateWorktreeId(). */
47
+ export function assertValidWorktreeId(worktreeId) {
48
+ if (worktreeId.length > WORKTREE_ID_MAX_LENGTH ||
49
+ !WORKTREE_ID_PATTERN.test(worktreeId)) {
50
+ throw new PatchWardenError("invalid_worktree_id", "Invalid worktree id. Expected the wt_<base36 timestamp>_<12 hex chars> format.", "Use the worktree_id returned by create_subgoal_task.", true, { worktree_id_length: worktreeId.length });
51
+ }
52
+ }
40
53
  // ── 目录路径解析 ──────────────────────────────────────────────────
41
54
  /**
42
55
  * 返回 `<workspaceRoot>/_workspacetrees` 目录路径。不自动创建目录。
43
56
  */
44
57
  export function getWorktreesDir(workspaceRoot) {
45
- return join(resolveWorkspaceRoot(workspaceRoot), WorktreeDir);
58
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
59
+ return guardPath(join(wsRoot, WorktreeDir), wsRoot, WorktreeDir);
46
60
  }
47
61
  /**
48
62
  * 返回 `<getWorktreesDir()>/<worktreeId>` 路径。
49
63
  */
50
64
  export function getWorktreeDir(worktreeId, workspaceRoot) {
51
- return join(getWorktreesDir(workspaceRoot), worktreeId);
65
+ assertValidWorktreeId(worktreeId);
66
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
67
+ return guardPath(join(getWorktreesDir(wsRoot), worktreeId), wsRoot, WorktreeDir);
52
68
  }
53
69
  // ── ID 与 branch 生成 ─────────────────────────────────────────────
54
70
  /**
@@ -70,21 +86,76 @@ function sanitizeBranchSegment(value) {
70
86
  const cleaned = value.replace(/[^a-zA-Z0-9_-]/g, "_");
71
87
  return cleaned === "" ? "x" : cleaned;
72
88
  }
89
+ function samePath(left, right) {
90
+ const normalizedLeft = normalize(left);
91
+ const normalizedRight = normalize(right);
92
+ return process.platform === "win32"
93
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
94
+ : normalizedLeft === normalizedRight;
95
+ }
96
+ function withWorktreeRepositoryLock(workspaceRoot, action) {
97
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
98
+ const lockDir = guardPath(join(wsRoot, WORKTREE_LOCK_DIR), wsRoot, WORKTREE_LOCK_DIR);
99
+ mkdirSync(lockDir, { recursive: true });
100
+ return withFileLockSync(join(lockDir, "repository-mutation"), action, {
101
+ waitMs: 0,
102
+ busyError: () => new PatchWardenError("worktree_busy", "Another managed worktree operation is already changing this repository.", "Retry after the active create, merge, or discard operation finishes.", true),
103
+ });
104
+ }
105
+ function invalidWorktreeStatus(worktreeId, message) {
106
+ throw new PatchWardenError("invalid_worktree_status", `Invalid worktree status for "${worktreeId}": ${message}`, "Do not edit worktree_status.json manually; recreate the isolated worktree.", true, { worktree_id: worktreeId });
107
+ }
108
+ function validateWorktreeStatus(value, worktreeId, workspaceRoot, expectedPath) {
109
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
110
+ return invalidWorktreeStatus(worktreeId, "expected a JSON object");
111
+ }
112
+ const candidate = value;
113
+ const statusValues = new Set(["active", "merged", "discarded"]);
114
+ if (typeof candidate.worktree_id !== "string" ||
115
+ typeof candidate.goal_id !== "string" ||
116
+ typeof candidate.subgoal_id !== "string" ||
117
+ typeof candidate.path !== "string" ||
118
+ typeof candidate.created_at !== "string" ||
119
+ typeof candidate.status !== "string" ||
120
+ !statusValues.has(candidate.status) ||
121
+ typeof candidate.branch !== "string") {
122
+ return invalidWorktreeStatus(worktreeId, "missing or invalid required fields");
123
+ }
124
+ if (candidate.worktree_id !== worktreeId) {
125
+ return invalidWorktreeStatus(worktreeId, "worktree_id does not match its directory");
126
+ }
127
+ let guardedStatusPath;
128
+ try {
129
+ guardedStatusPath = guardPath(candidate.path, workspaceRoot, WorktreeDir);
130
+ }
131
+ catch {
132
+ return invalidWorktreeStatus(worktreeId, "path is outside the managed worktree root");
133
+ }
134
+ if (!samePath(guardedStatusPath, expectedPath)) {
135
+ return invalidWorktreeStatus(worktreeId, "path does not match its directory");
136
+ }
137
+ const expectedBranch = `pw-${sanitizeBranchSegment(candidate.goal_id)}-${sanitizeBranchSegment(candidate.subgoal_id)}`;
138
+ if (candidate.branch !== expectedBranch) {
139
+ return invalidWorktreeStatus(worktreeId, "branch does not match goal and subgoal metadata");
140
+ }
141
+ if ((candidate.merged_at !== undefined && typeof candidate.merged_at !== "string") ||
142
+ (candidate.discarded_at !== undefined && typeof candidate.discarded_at !== "string")) {
143
+ return invalidWorktreeStatus(worktreeId, "invalid lifecycle timestamp");
144
+ }
145
+ return candidate;
146
+ }
73
147
  // ── 原子写 ────────────────────────────────────────────────────────
74
- /**
75
- * 原子写入 status 文件:先写到 `.tmp` 文件,再 renameSync。
76
- * 参考 goalStore.ts writeGoalStatus 的模式。
77
- */
78
148
  function writeStatusAtomic(statusFilePath, status) {
79
- const tmpPath = statusFilePath + ".tmp";
80
- writeFileSync(tmpPath, JSON.stringify(status, null, 2) + "\n", "utf-8");
81
- renameSync(tmpPath, statusFilePath);
149
+ atomicWriteJsonFileSync(statusFilePath, status);
82
150
  }
83
151
  function runGit(args, cwd, timeoutMs) {
84
- execFileSync("git", args, {
152
+ const env = buildGitEnvironment(cwd);
153
+ execFileSync(resolveTrustedExecutable("git", cwd, { pathValue: env.PATH }), args, {
85
154
  cwd,
86
155
  timeout: timeoutMs,
87
156
  stdio: ["pipe", "pipe", "pipe"],
157
+ windowsHide: true,
158
+ env,
88
159
  });
89
160
  }
90
161
  function gitErrorMessage(err) {
@@ -114,62 +185,64 @@ function gitErrorMessage(err) {
114
185
  * @returns { worktreeId, worktreePath, branch }
115
186
  */
116
187
  export function createWorktree(goalId, subgoalId, workspaceRoot) {
117
- const wsRoot = resolveWorkspaceRoot(workspaceRoot);
118
- const worktreeId = generateWorktreeId();
119
- const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
120
- const branch = `pw-${sanitizeBranchSegment(goalId)}-${sanitizeBranchSegment(subgoalId)}`;
121
- // 安全:校验 worktreePath 在 workspaceRoot 内且非敏感路径
122
- guardWorkspacePath(worktreePath, wsRoot);
123
- guardSensitivePath(worktreePath);
124
- let worktreeCreated = false;
125
- let branchCreated = false;
126
- try {
127
- runGit(["worktree", "add", "-b", branch, worktreePath], wsRoot, GIT_TIMEOUT_MS);
128
- worktreeCreated = true;
129
- branchCreated = true;
130
- // 原子写入 worktree_status.json(worktreePath 是新目录,无旧 status,直接 tmp + rename)
131
- const statusFilePath = join(worktreePath, "worktree_status.json");
132
- const now = new Date().toISOString();
133
- const status = {
134
- worktree_id: worktreeId,
135
- goal_id: goalId,
136
- subgoal_id: subgoalId,
137
- path: worktreePath,
138
- created_at: now,
139
- status: "active",
140
- branch,
141
- };
142
- writeStatusAtomic(statusFilePath, status);
143
- return { worktreeId, worktreePath, branch };
144
- }
145
- catch (err) {
146
- // 清理半成品
147
- if (worktreeCreated) {
148
- try {
149
- rmSync(worktreePath, { recursive: true, force: true });
150
- }
151
- catch { /* ignore */ }
152
- // rmSync 可能无法清除 git 的 worktree 元数据,再尝试 git worktree remove
153
- try {
154
- runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_BRANCH_TIMEOUT_MS);
155
- }
156
- catch { /* ignore */ }
188
+ return withWorktreeRepositoryLock(workspaceRoot, () => {
189
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
190
+ const worktreeId = generateWorktreeId();
191
+ const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
192
+ const branch = `pw-${sanitizeBranchSegment(goalId)}-${sanitizeBranchSegment(subgoalId)}`;
193
+ // 安全:校验 worktreePath 在 workspaceRoot 内且非敏感路径
194
+ guardWorkspacePath(worktreePath, wsRoot);
195
+ guardSensitivePath(worktreePath);
196
+ let worktreeCreated = false;
197
+ let branchCreated = false;
198
+ try {
199
+ runGit(["worktree", "add", "-b", branch, worktreePath], wsRoot, GIT_TIMEOUT_MS);
200
+ worktreeCreated = true;
201
+ branchCreated = true;
202
+ // 原子写入 worktree_status.json(worktreePath 是新目录,无旧 status,直接 tmp + rename)
203
+ const statusFilePath = join(worktreePath, "worktree_status.json");
204
+ const now = new Date().toISOString();
205
+ const status = {
206
+ worktree_id: worktreeId,
207
+ goal_id: goalId,
208
+ subgoal_id: subgoalId,
209
+ path: worktreePath,
210
+ created_at: now,
211
+ status: "active",
212
+ branch,
213
+ };
214
+ writeStatusAtomic(statusFilePath, status);
215
+ return { worktreeId, worktreePath, branch };
157
216
  }
158
- if (branchCreated) {
159
- try {
160
- runGit(["branch", "-D", branch], wsRoot, GIT_BRANCH_TIMEOUT_MS);
217
+ catch (err) {
218
+ // 清理半成品
219
+ if (worktreeCreated) {
220
+ try {
221
+ rmSync(worktreePath, { recursive: true, force: true });
222
+ }
223
+ catch { /* ignore */ }
224
+ // rmSync 可能无法清除 git 的 worktree 元数据,再尝试 git worktree remove
225
+ try {
226
+ runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_BRANCH_TIMEOUT_MS);
227
+ }
228
+ catch { /* ignore */ }
161
229
  }
162
- catch { /* ignore — branch 可能未创建或已随 worktree remove 清理 */ }
230
+ if (branchCreated) {
231
+ try {
232
+ runGit(["branch", "-D", branch], wsRoot, GIT_BRANCH_TIMEOUT_MS);
233
+ }
234
+ catch { /* ignore — branch 可能未创建或已随 worktree remove 清理 */ }
235
+ }
236
+ if (err instanceof PatchWardenError)
237
+ throw err;
238
+ throw new PatchWardenError("worktree_create_failed", `Failed to create worktree for goal "${goalId}" / subgoal "${subgoalId}": ${gitErrorMessage(err)}`, "Ensure workspaceRoot is a git repository with at least one commit, and the worktree path is writable.", true, {
239
+ goal_id: goalId,
240
+ subgoal_id: subgoalId,
241
+ branch,
242
+ worktree_path: worktreePath,
243
+ });
163
244
  }
164
- if (err instanceof PatchWardenError)
165
- throw err;
166
- throw new PatchWardenError("worktree_create_failed", `Failed to create worktree for goal "${goalId}" / subgoal "${subgoalId}": ${gitErrorMessage(err)}`, "Ensure workspaceRoot is a git repository with at least one commit, and the worktree path is writable.", true, {
167
- goal_id: goalId,
168
- subgoal_id: subgoalId,
169
- branch,
170
- worktree_path: worktreePath,
171
- });
172
- }
245
+ });
173
246
  }
174
247
  /**
175
248
  * 读取 worktree_status.json。不存在返回 null。
@@ -183,13 +256,14 @@ export function readWorktreeStatus(worktreeId, workspaceRoot) {
183
256
  const statusFilePath = join(worktreePath, "worktree_status.json");
184
257
  if (!existsSync(statusFilePath))
185
258
  return null;
259
+ let parsed;
186
260
  try {
187
- const raw = readFileSync(statusFilePath, "utf-8");
188
- return JSON.parse(raw);
261
+ parsed = JSON.parse(readFileSync(statusFilePath, "utf-8"));
189
262
  }
190
263
  catch {
191
- return null;
264
+ return invalidWorktreeStatus(worktreeId, "file is not valid JSON");
192
265
  }
266
+ return validateWorktreeStatus(parsed, worktreeId, wsRoot, worktreePath);
193
267
  }
194
268
  /**
195
269
  * 将 worktree 的 branch 合并回主工作区(workspaceRoot)。
@@ -203,32 +277,34 @@ export function readWorktreeStatus(worktreeId, workspaceRoot) {
203
277
  * (保留供人工排查冲突)。
204
278
  */
205
279
  export function mergeWorktree(worktreeId, workspaceRoot) {
206
- const wsRoot = resolveWorkspaceRoot(workspaceRoot);
207
- const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
208
- guardWorkspacePath(worktreePath, wsRoot);
209
- guardSensitivePath(worktreePath);
210
- const status = readWorktreeStatus(worktreeId, workspaceRoot);
211
- if (!status) {
212
- throw new PatchWardenError("worktree_not_found", `Worktree "${worktreeId}" not found or has no worktree_status.json`, "Ensure the worktree id was created via createWorktree before merging.", true, { worktree_id: worktreeId });
213
- }
214
- if (status.status !== "active") {
215
- throw new PatchWardenError("invalid_worktree_state", `Worktree "${worktreeId}" is not active (current status: "${status.status}")`, "Only active worktrees can be merged.", true, { worktree_id: worktreeId, current_status: status.status });
216
- }
217
- try {
218
- runGit(["merge", status.branch], wsRoot, GIT_TIMEOUT_MS);
219
- }
220
- catch (err) {
221
- // 合并失败:不删 worktree,保留供人工排查
222
- throw new PatchWardenError("worktree_merge_failed", `Failed to merge worktree branch "${status.branch}" into workspace: ${gitErrorMessage(err)}`, "Resolve merge conflicts manually in the main workspace, then retry or discard the worktree.", true, { worktree_id: worktreeId, branch: status.branch });
223
- }
224
- const updatedStatus = {
225
- ...status,
226
- status: "merged",
227
- merged_at: new Date().toISOString(),
228
- };
229
- const statusFilePath = join(worktreePath, "worktree_status.json");
230
- writeStatusAtomic(statusFilePath, updatedStatus);
231
- return { status: "merged" };
280
+ return withWorktreeRepositoryLock(workspaceRoot, () => {
281
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
282
+ const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
283
+ guardWorkspacePath(worktreePath, wsRoot);
284
+ guardSensitivePath(worktreePath);
285
+ const status = readWorktreeStatus(worktreeId, workspaceRoot);
286
+ if (!status) {
287
+ throw new PatchWardenError("worktree_not_found", `Worktree "${worktreeId}" not found or has no worktree_status.json`, "Ensure the worktree id was created via createWorktree before merging.", true, { worktree_id: worktreeId });
288
+ }
289
+ if (status.status !== "active") {
290
+ throw new PatchWardenError("invalid_worktree_state", `Worktree "${worktreeId}" is not active (current status: "${status.status}")`, "Only active worktrees can be merged.", true, { worktree_id: worktreeId, current_status: status.status });
291
+ }
292
+ try {
293
+ runGit(["merge", status.branch], wsRoot, GIT_TIMEOUT_MS);
294
+ }
295
+ catch (err) {
296
+ // 合并失败:不删 worktree,保留供人工排查
297
+ throw new PatchWardenError("worktree_merge_failed", `Failed to merge worktree branch "${status.branch}" into workspace: ${gitErrorMessage(err)}`, "Resolve merge conflicts manually in the main workspace, then retry or discard the worktree.", true, { worktree_id: worktreeId, branch: status.branch });
298
+ }
299
+ const updatedStatus = {
300
+ ...status,
301
+ status: "merged",
302
+ merged_at: new Date().toISOString(),
303
+ };
304
+ const statusFilePath = join(worktreePath, "worktree_status.json");
305
+ writeStatusAtomic(statusFilePath, updatedStatus);
306
+ return { status: "merged" };
307
+ });
232
308
  }
233
309
  /**
234
310
  * 丢弃 worktree:移除 worktree 目录与临时 branch,归档最终状态。
@@ -245,48 +321,45 @@ export function mergeWorktree(worktreeId, workspaceRoot) {
245
321
  * 移除失败抛 PatchWardenError("worktree_discard_failed")。
246
322
  */
247
323
  export function discardWorktree(worktreeId, workspaceRoot) {
248
- const wsRoot = resolveWorkspaceRoot(workspaceRoot);
249
- const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
250
- guardWorkspacePath(worktreePath, wsRoot);
251
- guardSensitivePath(worktreePath);
252
- // 在 remove 之前读取 status(status 文件位于 worktree 目录内)
253
- const status = readWorktreeStatus(worktreeId, workspaceRoot);
254
- if (!status) {
255
- throw new PatchWardenError("worktree_not_found", `Worktree "${worktreeId}" not found or has no worktree_status.json`, "Ensure the worktree id was created via createWorktree before discarding.", true, { worktree_id: worktreeId });
256
- }
257
- if (status.status !== "active") {
258
- throw new PatchWardenError("invalid_worktree_state", `Worktree "${worktreeId}" is not active (current status: "${status.status}")`, "Only active worktrees can be discarded.", true, { worktree_id: worktreeId, current_status: status.status });
259
- }
260
- try {
261
- runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_TIMEOUT_MS);
262
- }
263
- catch (err) {
264
- // git remove 失败时再尝试直接删目录(best effort),然后抛错
324
+ return withWorktreeRepositoryLock(workspaceRoot, () => {
325
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
326
+ const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
327
+ guardWorkspacePath(worktreePath, wsRoot);
328
+ guardSensitivePath(worktreePath);
329
+ // 在 remove 之前读取 status(status 文件位于 worktree 目录内)
330
+ const status = readWorktreeStatus(worktreeId, workspaceRoot);
331
+ if (!status) {
332
+ throw new PatchWardenError("worktree_not_found", `Worktree "${worktreeId}" not found or has no worktree_status.json`, "Ensure the worktree id was created via createWorktree before discarding.", true, { worktree_id: worktreeId });
333
+ }
334
+ if (status.status !== "active") {
335
+ throw new PatchWardenError("invalid_worktree_state", `Worktree "${worktreeId}" is not active (current status: "${status.status}")`, "Only active worktrees can be discarded.", true, { worktree_id: worktreeId, current_status: status.status });
336
+ }
265
337
  try {
266
- rmSync(worktreePath, { recursive: true, force: true });
338
+ runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_TIMEOUT_MS);
339
+ }
340
+ catch (err) {
341
+ throw new PatchWardenError("worktree_discard_failed", `Failed to remove worktree "${worktreeId}": ${gitErrorMessage(err)}`, "Remove the worktree directory manually and run `git worktree prune`.", true, { worktree_id: worktreeId, worktree_path: worktreePath });
342
+ }
343
+ // 删除临时 branch(best effort — 可能已随 worktree remove 清理)
344
+ try {
345
+ runGit(["branch", "-D", status.branch], wsRoot, GIT_BRANCH_TIMEOUT_MS);
267
346
  }
268
347
  catch { /* ignore */ }
269
- throw new PatchWardenError("worktree_discard_failed", `Failed to remove worktree "${worktreeId}": ${gitErrorMessage(err)}`, "Remove the worktree directory manually and run `git worktree prune`.", true, { worktree_id: worktreeId, worktree_path: worktreePath });
270
- }
271
- // 删除临时 branch(best effort — 可能已随 worktree remove 清理)
272
- try {
273
- runGit(["branch", "-D", status.branch], wsRoot, GIT_BRANCH_TIMEOUT_MS);
274
- }
275
- catch { /* ignore */ }
276
- // 归档最终状态到 .patchwarden/worktree-archive/(worktree 目录可能已消失)
277
- const archivedStatus = {
278
- ...status,
279
- status: "discarded",
280
- discarded_at: new Date().toISOString(),
281
- };
282
- const archiveDir = join(wsRoot, WORKTREE_ARCHIVE_DIR);
283
- guardWorkspacePath(archiveDir, wsRoot);
284
- // .patchwarden 路径在 sensitiveGuard 中始终视为安全(SAFE_PREFIX),无需额外校验
285
- try {
286
- mkdirSync(archiveDir, { recursive: true });
287
- }
288
- catch { /* ignore */ }
289
- const archiveFilePath = join(archiveDir, `${worktreeId}.json`);
290
- writeStatusAtomic(archiveFilePath, archivedStatus);
291
- return { status: "discarded" };
348
+ // 归档最终状态到 .patchwarden/worktree-archive/(worktree 目录可能已消失)
349
+ const archivedStatus = {
350
+ ...status,
351
+ status: "discarded",
352
+ discarded_at: new Date().toISOString(),
353
+ };
354
+ const archiveDir = join(wsRoot, WORKTREE_ARCHIVE_DIR);
355
+ guardWorkspacePath(archiveDir, wsRoot);
356
+ // The archive path is workspace-confined and contains only managed status metadata.
357
+ try {
358
+ mkdirSync(archiveDir, { recursive: true });
359
+ }
360
+ catch { /* ignore */ }
361
+ const archiveFilePath = join(archiveDir, `${worktreeId}.json`);
362
+ writeStatusAtomic(archiveFilePath, archivedStatus);
363
+ return { status: "discarded" };
364
+ });
292
365
  }
@@ -15,16 +15,21 @@
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
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";
28
33
  // ── Bootstrap ─────────────────────────────────────────────────────
29
34
  const config = loadConfig();
30
35
  const port = parseInt(process.env.PATCHWARDEN_HTTP_PORT || "") ||
@@ -38,6 +43,16 @@ logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed
38
43
  const httpCfg = config.http || {};
39
44
  const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
40
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
+ ]);
41
56
  if (ownerToken) {
42
57
  logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
43
58
  }
@@ -47,13 +62,13 @@ else {
47
62
  function checkOwnerToken(req) {
48
63
  if (!ownerToken)
49
64
  return true; // no token configured — allow all
50
- const authHeader = req.headers["authorization"] || "";
51
- const customHeader = req.headers["x-patchwarden-token"] || "";
65
+ const authHeader = firstHeaderValue(req.headers["authorization"]);
66
+ const customHeader = firstHeaderValue(req.headers["x-patchwarden-token"]);
52
67
  if (authHeader.startsWith("Bearer ")) {
53
- return authHeader.slice(7) === ownerToken;
68
+ return timingSafeStringEqual(authHeader.slice(7), ownerToken);
54
69
  }
55
- if (typeof customHeader === "string" && customHeader.length > 0) {
56
- return customHeader === ownerToken;
70
+ if (customHeader.length > 0) {
71
+ return timingSafeStringEqual(customHeader, ownerToken);
57
72
  }
58
73
  return false;
59
74
  }
@@ -69,12 +84,23 @@ function handleAcceptance(taskId, status, body) {
69
84
  throw Object.assign(new Error(`Task "${taskId}" not found.`), { statusCode: 404 });
70
85
  }
71
86
  let notes = "";
72
- try {
73
- const parsed = JSON.parse(body || "{}");
74
- notes = String(parsed.notes || parsed.reason || "");
75
- }
76
- catch {
77
- // 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);
78
104
  }
79
105
  const acceptance = {
80
106
  status,
@@ -82,22 +108,67 @@ function handleAcceptance(taskId, status, body) {
82
108
  reviewer: "human",
83
109
  notes,
84
110
  };
85
- writeFileSync(filePath, JSON.stringify(acceptance, null, 2), "utf-8");
86
- // 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.
87
113
  const statusFile = join(taskDir, "status.json");
88
- if (existsSync(statusFile)) {
89
- try {
90
- const current = JSON.parse(readFileSync(statusFile, "utf-8"));
91
- current.acceptance_status = status;
92
- current.acceptance_reviewed_at = acceptance.reviewed_at;
93
- writeFileSync(statusFile, JSON.stringify(current, null, 2), "utf-8");
94
- }
95
- catch {
96
- // status.json update is best-effort
97
- }
114
+ if (!existsSync(statusFile)) {
115
+ throw httpError(409, `Task "${taskId}" has no status.json to review.`);
98
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
+ });
99
130
  return acceptance;
100
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
+ }
101
172
  function readAcceptance(taskId) {
102
173
  const filePath = getAcceptancePath(taskId);
103
174
  if (!existsSync(filePath)) {
@@ -164,6 +235,15 @@ function parseAdminUrl(url) {
164
235
  }
165
236
  // ── HTTP server ───────────────────────────────────────────────────
166
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
+ }
167
247
  // Health check endpoints
168
248
  if (req.method === "GET" && (req.url === "/healthz" || req.url === "/readyz")) {
169
249
  const health = healthCheck(getToolCatalogSnapshot());
@@ -188,11 +268,7 @@ const httpServer = createServer(async (req, res) => {
188
268
  return;
189
269
  }
190
270
  if ((admin.action === "accept" || admin.action === "reject") && req.method === "POST") {
191
- const chunks = [];
192
- for await (const chunk of req) {
193
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
194
- }
195
- const body = Buffer.concat(chunks).toString("utf-8");
271
+ const body = await readBoundedRequestBody(req, MAX_ADMIN_BODY_BYTES);
196
272
  const acceptance = handleAcceptance(admin.taskId, admin.action === "accept" ? "accepted" : "rejected", body);
197
273
  res.writeHead(200, { "Content-Type": "application/json" });
198
274
  res.end(JSON.stringify(acceptance, null, 2));
@@ -202,9 +278,12 @@ const httpServer = createServer(async (req, res) => {
202
278
  res.end(JSON.stringify({ error: "Method not allowed for this admin endpoint." }));
203
279
  }
204
280
  catch (err) {
205
- 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";
206
285
  res.writeHead(statusCode, { "Content-Type": "application/json" });
207
- res.end(JSON.stringify({ error: err.message || "Internal server error" }));
286
+ res.end(JSON.stringify({ error: message }));
208
287
  }
209
288
  return;
210
289
  }