patchwarden 1.5.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (591) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +158 -26
  3. package/README.md +124 -24
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
  6. package/dist/assessments/assessmentDiagnostics.js +132 -0
  7. package/dist/assessments/assessmentStore.d.ts +39 -0
  8. package/dist/assessments/assessmentStore.js +207 -45
  9. package/dist/assessments/securitySnapshot.d.ts +50 -0
  10. package/dist/assessments/securitySnapshot.js +158 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +66 -13
  13. package/dist/control/fileManager.d.ts +2 -0
  14. package/dist/control/fileManager.js +39 -0
  15. package/dist/control/middleware/auth.d.ts +6 -0
  16. package/dist/control/middleware/auth.js +14 -5
  17. package/dist/control/middleware/static.js +16 -3
  18. package/dist/control/routeTable.d.ts +24 -0
  19. package/dist/control/routeTable.js +263 -0
  20. package/dist/control/routes/audit.js +17 -7
  21. package/dist/control/routes/evidence.js +1 -1
  22. package/dist/control/routes/lineage.js +1 -1
  23. package/dist/control/routes/process.d.ts +7 -0
  24. package/dist/control/routes/process.js +329 -42
  25. package/dist/control/routes/sessions.d.ts +9 -1
  26. package/dist/control/routes/sessions.js +84 -21
  27. package/dist/control/routes/status.d.ts +60 -1
  28. package/dist/control/routes/status.js +94 -15
  29. package/dist/control/routes/taskActions.js +24 -60
  30. package/dist/control/routes/tasks.d.ts +4 -0
  31. package/dist/control/routes/tasks.js +61 -62
  32. package/dist/control/routes/workspace.js +50 -23
  33. package/dist/control/runtime.d.ts +3 -2
  34. package/dist/control/runtime.js +65 -40
  35. package/dist/control/server.js +37 -365
  36. package/dist/control/shared.d.ts +25 -1
  37. package/dist/control/shared.js +64 -11
  38. package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
  39. package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
  40. package/dist/direct/directAudit.js +234 -225
  41. package/dist/direct/directGuards.d.ts +2 -1
  42. package/dist/direct/directGuards.js +42 -29
  43. package/dist/direct/directPatch.d.ts +6 -1
  44. package/dist/direct/directPatch.js +55 -8
  45. package/dist/direct/directSessionStore.d.ts +16 -6
  46. package/dist/direct/directSessionStore.js +128 -35
  47. package/dist/direct/directVerification.js +8 -16
  48. package/dist/doctor.js +65 -57
  49. package/dist/goal/acceptanceEngine.d.ts +1 -1
  50. package/dist/goal/goalProgress.js +75 -69
  51. package/dist/goal/goalReport.js +5 -12
  52. package/dist/goal/goalStatus.d.ts +2 -0
  53. package/dist/goal/goalStatus.js +10 -3
  54. package/dist/goal/goalStore.d.ts +9 -2
  55. package/dist/goal/goalStore.js +152 -45
  56. package/dist/goal/handoffExport.js +6 -6
  57. package/dist/goal/specKitImport.js +97 -96
  58. package/dist/goal/subgoalSync.js +11 -9
  59. package/dist/goal/worktreeManager.d.ts +2 -0
  60. package/dist/goal/worktreeManager.js +209 -136
  61. package/dist/httpServer.js +111 -32
  62. package/dist/logging.js +5 -3
  63. package/dist/policy/projectPolicy.js +5 -2
  64. package/dist/release/releaseGate.d.ts +1 -1
  65. package/dist/release/releaseGate.js +78 -25
  66. package/dist/runner/agentInvocation.d.ts +14 -0
  67. package/dist/runner/agentInvocation.js +96 -4
  68. package/dist/runner/changeCapture.d.ts +13 -5
  69. package/dist/runner/changeCapture.js +243 -61
  70. package/dist/runner/postTaskCleanup.js +48 -19
  71. package/dist/runner/processSecurity.d.ts +56 -0
  72. package/dist/runner/processSecurity.js +373 -0
  73. package/dist/runner/runTask.d.ts +2 -1
  74. package/dist/runner/runTask.js +162 -93
  75. package/dist/runner/simpleProcess.d.ts +3 -0
  76. package/dist/runner/simpleProcess.js +50 -36
  77. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  78. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  79. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +3 -1
  80. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  81. package/dist/runner/taskStatusStore.d.ts +11 -0
  82. package/dist/runner/taskStatusStore.js +35 -0
  83. package/dist/runner/watch.d.ts +14 -1
  84. package/dist/runner/watch.js +320 -49
  85. package/dist/security/contentRedaction.js +2 -2
  86. package/dist/security/discoveryTokenStore.d.ts +3 -2
  87. package/dist/security/discoveryTokenStore.js +17 -2
  88. package/dist/security/loopbackHost.d.ts +2 -0
  89. package/dist/security/loopbackHost.js +16 -0
  90. package/dist/security/pathGuard.js +32 -17
  91. package/dist/security/planGuard.js +2 -1
  92. package/dist/security/runtimeGuard.js +9 -6
  93. package/dist/security/secretComparison.d.ts +3 -0
  94. package/dist/security/secretComparison.js +11 -0
  95. package/dist/security/sensitiveGuard.d.ts +5 -0
  96. package/dist/security/sensitiveGuard.js +27 -8
  97. package/dist/security/toolInvocationGuard.d.ts +2 -2
  98. package/dist/security/toolInvocationGuard.js +1 -1
  99. package/dist/security/workspaceRootGuard.d.ts +8 -0
  100. package/dist/security/workspaceRootGuard.js +47 -0
  101. package/dist/smoke-test.js +27 -26
  102. package/dist/tools/catalog/index.d.ts +4 -0
  103. package/dist/tools/catalog/index.js +4 -0
  104. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  105. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  106. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  107. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  108. package/dist/tools/definitions/toolDefs.js +1177 -0
  109. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  110. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  111. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +2 -1
  112. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
  113. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
  114. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
  115. package/dist/tools/diagnostics/index.d.ts +6 -0
  116. package/dist/tools/diagnostics/index.js +6 -0
  117. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  118. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  119. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  120. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  121. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  122. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  123. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  124. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  125. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  126. package/dist/tools/direct/index.d.ts +3 -0
  127. package/dist/tools/direct/index.js +3 -0
  128. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  129. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  130. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  131. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  132. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  133. package/dist/tools/discovery/index.d.ts +3 -0
  134. package/dist/tools/discovery/index.js +3 -0
  135. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  136. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  137. package/dist/tools/dispatch/coreDispatch.js +32 -31
  138. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  139. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  140. package/dist/tools/dispatch/directDispatch.js +11 -10
  141. package/dist/tools/dispatch/goalDispatch.js +5 -4
  142. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  143. package/dist/tools/dispatch/validation.d.ts +7 -0
  144. package/dist/tools/dispatch/validation.js +71 -0
  145. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  146. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  147. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  148. package/dist/tools/goals/index.d.ts +3 -0
  149. package/dist/tools/goals/index.js +3 -0
  150. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  151. package/dist/tools/registry.d.ts +7 -11
  152. package/dist/tools/registry.js +17 -1178
  153. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  154. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  155. package/dist/tools/release/index.d.ts +2 -0
  156. package/dist/tools/release/index.js +2 -0
  157. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  158. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  159. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  160. package/dist/tools/tasks/cancelTask.js +86 -0
  161. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  162. package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
  163. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  164. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
  165. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  166. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  167. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  168. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +11 -6
  169. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  170. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
  171. package/dist/tools/tasks/index.d.ts +18 -0
  172. package/dist/tools/tasks/index.js +18 -0
  173. package/dist/tools/tasks/killTask.d.ts +1 -0
  174. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  175. package/dist/tools/{listTasks.js → tasks/listTasks.js} +5 -4
  176. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
  177. package/dist/tools/tasks/reconcileTasks.js +496 -0
  178. package/dist/tools/tasks/retryTask.js +60 -0
  179. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  180. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +63 -41
  181. package/dist/tools/tasks/runVerification.js +38 -0
  182. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  183. package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
  184. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  185. package/dist/tools/tasks/taskStates.d.ts +6 -0
  186. package/dist/tools/tasks/taskStates.js +52 -0
  187. package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
  188. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  189. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  190. package/dist/tools/workspace/applyPatch.js +53 -0
  191. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  192. package/dist/tools/workspace/index.d.ts +10 -0
  193. package/dist/tools/workspace/index.js +10 -0
  194. package/dist/tools/workspace/listAgents.d.ts +23 -0
  195. package/dist/tools/workspace/listAgents.js +56 -0
  196. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  197. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  198. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  199. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  200. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  201. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  202. package/dist/tools/workspace/syncFile.js +134 -0
  203. package/dist/utils/atomicFile.d.ts +7 -0
  204. package/dist/utils/atomicFile.js +67 -0
  205. package/dist/utils/boundedFile.d.ts +8 -0
  206. package/dist/utils/boundedFile.js +93 -0
  207. package/dist/utils/lockedJsonFile.d.ts +15 -0
  208. package/dist/utils/lockedJsonFile.js +224 -0
  209. package/dist/utils/platform.d.ts +11 -0
  210. package/dist/utils/platform.js +26 -0
  211. package/dist/utils/stableJson.d.ts +2 -0
  212. package/dist/utils/stableJson.js +12 -0
  213. package/dist/version.d.ts +3 -2
  214. package/dist/version.js +3 -2
  215. package/dist/watcherStatus.d.ts +2 -2
  216. package/dist/watcherStatus.js +18 -0
  217. package/docs/CODE_WIKI.md +808 -143
  218. package/docs/architecture.md +15 -55
  219. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  220. package/docs/control-center/README.md +4 -4
  221. package/docs/dashboard-overview.md +4 -4
  222. package/docs/desktop-app.md +175 -0
  223. package/docs/evidence-pack-schema.md +3 -3
  224. package/docs/open-source-application.md +16 -11
  225. package/docs/release-checklist.md +1 -1
  226. package/docs/release-evidence.md +35 -39
  227. package/docs/task-safe-review-workflow.md +5 -5
  228. package/docs/threat-model.md +19 -1
  229. package/examples/config.example.json +4 -2
  230. package/package.json +27 -10
  231. package/scripts/README.md +15 -6
  232. package/scripts/build.js +11 -0
  233. package/scripts/checks/brand-check.js +3 -0
  234. package/scripts/checks/build-output-check.js +37 -0
  235. package/scripts/checks/control-center-smoke.js +102 -10
  236. package/scripts/checks/control-smoke.js +19 -8
  237. package/scripts/checks/http-mcp-smoke.js +82 -4
  238. package/scripts/checks/lifecycle-smoke.js +15 -12
  239. package/scripts/checks/mcp-manifest-check.js +1 -1
  240. package/scripts/checks/mcp-smoke.js +58 -4
  241. package/scripts/checks/package-manifest-check.js +20 -7
  242. package/scripts/checks/unit-tests.js +3 -5
  243. package/scripts/checks/watcher-supervisor-smoke.js +38 -1
  244. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  245. package/scripts/control/manage-patchwarden.ps1 +69 -4
  246. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  247. package/scripts/control/run-background-supervisor.ps1 +41 -0
  248. package/scripts/control/start-patchwarden-tunnel.ps1 +191 -33
  249. package/scripts/e2e/demo-runtime.mjs +153 -0
  250. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  251. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  252. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  253. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  254. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  255. package/scripts/lib/clean-generated-output.js +11 -0
  256. package/scripts/lib/file-discovery.js +12 -0
  257. package/scripts/release/desktop-preflight.js +211 -0
  258. package/scripts/release/pack-clean.js +59 -14
  259. package/src/assessments/agentAssessor.ts +26 -12
  260. package/src/assessments/assessmentDiagnostics.ts +172 -0
  261. package/src/assessments/assessmentStore.ts +288 -51
  262. package/src/assessments/securitySnapshot.ts +231 -0
  263. package/src/config.ts +70 -13
  264. package/src/control/fileManager.ts +40 -0
  265. package/src/control/middleware/auth.ts +22 -4
  266. package/src/control/middleware/static.ts +16 -3
  267. package/src/control/routeTable.ts +301 -0
  268. package/src/control/routes/audit.ts +14 -6
  269. package/src/control/routes/evidence.ts +1 -1
  270. package/src/control/routes/lineage.ts +1 -1
  271. package/src/control/routes/process.ts +341 -40
  272. package/src/control/routes/sessions.ts +88 -25
  273. package/src/control/routes/status.ts +134 -17
  274. package/src/control/routes/taskActions.ts +24 -58
  275. package/src/control/routes/tasks.ts +66 -58
  276. package/src/control/routes/workspace.ts +56 -25
  277. package/src/control/runtime.ts +71 -43
  278. package/src/control/server.ts +37 -342
  279. package/src/control/shared.ts +77 -11
  280. package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
  281. package/src/direct/directAudit.ts +17 -9
  282. package/src/direct/directGuards.ts +72 -44
  283. package/src/direct/directPatch.ts +95 -8
  284. package/src/direct/directSessionStore.ts +213 -55
  285. package/src/direct/directVerification.ts +9 -23
  286. package/src/doctor.ts +76 -64
  287. package/src/goal/acceptanceEngine.ts +1 -1
  288. package/src/goal/goalProgress.ts +17 -13
  289. package/src/goal/goalReport.ts +5 -13
  290. package/src/goal/goalStatus.ts +19 -3
  291. package/src/goal/goalStore.ts +241 -54
  292. package/src/goal/handoffExport.ts +6 -6
  293. package/src/goal/specKitImport.ts +12 -12
  294. package/src/goal/subgoalSync.ts +11 -8
  295. package/src/goal/worktreeManager.ts +139 -22
  296. package/src/httpServer.ts +113 -30
  297. package/src/logging.ts +10 -3
  298. package/src/policy/projectPolicy.ts +5 -2
  299. package/src/release/releaseGate.ts +74 -27
  300. package/src/runner/agentInvocation.ts +126 -4
  301. package/src/runner/changeCapture.ts +291 -57
  302. package/src/runner/postTaskCleanup.ts +41 -17
  303. package/src/runner/processSecurity.ts +433 -0
  304. package/src/runner/runTask.ts +197 -97
  305. package/src/runner/simpleProcess.ts +66 -31
  306. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  307. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
  308. package/src/runner/taskStatusStore.ts +69 -0
  309. package/src/runner/watch.ts +348 -52
  310. package/src/security/contentRedaction.ts +2 -2
  311. package/src/security/discoveryTokenStore.ts +18 -4
  312. package/src/security/loopbackHost.ts +19 -0
  313. package/src/security/pathGuard.ts +35 -16
  314. package/src/security/planGuard.ts +2 -1
  315. package/src/security/runtimeGuard.ts +9 -6
  316. package/src/security/secretComparison.ts +13 -0
  317. package/src/security/sensitiveGuard.ts +27 -8
  318. package/src/security/toolInvocationGuard.ts +2 -2
  319. package/src/security/workspaceRootGuard.ts +53 -0
  320. package/src/smoke-test.ts +27 -26
  321. package/src/tools/catalog/index.ts +4 -0
  322. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  323. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  324. package/src/tools/definitions/toolDefs.ts +1264 -0
  325. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  326. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +136 -39
  327. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
  328. package/src/tools/diagnostics/index.ts +6 -0
  329. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  330. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  331. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  332. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  333. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  334. package/src/tools/direct/index.ts +3 -0
  335. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  336. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  337. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  338. package/src/tools/discovery/index.ts +3 -0
  339. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  340. package/src/tools/dispatch/coreDispatch.ts +36 -31
  341. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  342. package/src/tools/dispatch/directDispatch.ts +11 -10
  343. package/src/tools/dispatch/goalDispatch.ts +5 -4
  344. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  345. package/src/tools/dispatch/validation.ts +78 -0
  346. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  347. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  348. package/src/tools/goals/index.ts +3 -0
  349. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  350. package/src/tools/registry.ts +17 -1261
  351. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  352. package/src/tools/release/index.ts +2 -0
  353. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  354. package/src/tools/tasks/cancelTask.ts +110 -0
  355. package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
  356. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
  357. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  358. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  359. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +13 -6
  360. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  361. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
  362. package/src/tools/tasks/index.ts +18 -0
  363. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
  364. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
  365. package/src/tools/tasks/retryTask.ts +69 -0
  366. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
  367. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  368. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  369. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  370. package/src/tools/tasks/taskStates.ts +54 -0
  371. package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
  372. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  373. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  374. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  375. package/src/tools/workspace/index.ts +10 -0
  376. package/src/tools/workspace/listAgents.ts +75 -0
  377. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  378. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  379. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  380. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  381. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  382. package/src/tools/workspace/syncFile.ts +292 -0
  383. package/src/utils/atomicFile.ts +74 -0
  384. package/src/utils/boundedFile.ts +116 -0
  385. package/src/utils/lockedJsonFile.ts +251 -0
  386. package/src/utils/platform.ts +31 -0
  387. package/src/utils/stableJson.ts +12 -0
  388. package/src/version.ts +3 -2
  389. package/src/watcherStatus.ts +19 -1
  390. package/ui/desktop-bootstrap.js +11 -0
  391. package/ui/desktop-bridge.js +36 -0
  392. package/ui/desktop.css +160 -0
  393. package/ui/getting-started.js +93 -0
  394. package/ui/i18n.js +484 -0
  395. package/ui/log-parser.js +30 -0
  396. package/ui/pages/audit.html +12 -0
  397. package/ui/pages/dashboard.html +94 -147
  398. package/ui/pages/direct-sessions.html +123 -26
  399. package/ui/pages/getting-started.html +43 -0
  400. package/ui/pages/logs.html +134 -22
  401. package/ui/pages/settings.html +101 -0
  402. package/ui/pages/task-detail.html +12 -0
  403. package/ui/pages/tasks.html +54 -8
  404. package/ui/pages/workspace.html +38 -1
  405. package/ui/settings.js +254 -0
  406. package/PatchWarden-Control.cmd +0 -6
  407. package/Restart-PatchWarden-Control.cmd +0 -6
  408. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  409. package/dist/test/unit/acceptance-engine.test.js +0 -228
  410. package/dist/test/unit/agent-router.test.d.ts +0 -1
  411. package/dist/test/unit/agent-router.test.js +0 -287
  412. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  413. package/dist/test/unit/android-doctor.test.js +0 -118
  414. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  415. package/dist/test/unit/apply-patch.test.js +0 -225
  416. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  417. package/dist/test/unit/audit-checks.test.js +0 -350
  418. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  419. package/dist/test/unit/chinese-path.test.js +0 -91
  420. package/dist/test/unit/command-guard.test.d.ts +0 -1
  421. package/dist/test/unit/command-guard.test.js +0 -160
  422. package/dist/test/unit/create-task.test.d.ts +0 -1
  423. package/dist/test/unit/create-task.test.js +0 -197
  424. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  425. package/dist/test/unit/diagnose-task.test.js +0 -457
  426. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  427. package/dist/test/unit/direct-guards.test.js +0 -328
  428. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  429. package/dist/test/unit/discovery-token-store.test.js +0 -139
  430. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  431. package/dist/test/unit/evidence-pack.test.js +0 -224
  432. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  433. package/dist/test/unit/get-task-status.test.js +0 -174
  434. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  435. package/dist/test/unit/get-task-summary.test.js +0 -146
  436. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  437. package/dist/test/unit/goal-graph.test.js +0 -298
  438. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  439. package/dist/test/unit/goal-progress.test.js +0 -381
  440. package/dist/test/unit/goal-report.test.d.ts +0 -1
  441. package/dist/test/unit/goal-report.test.js +0 -159
  442. package/dist/test/unit/goal-status.test.d.ts +0 -1
  443. package/dist/test/unit/goal-status.test.js +0 -215
  444. package/dist/test/unit/goal-store.test.d.ts +0 -1
  445. package/dist/test/unit/goal-store.test.js +0 -253
  446. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  447. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  448. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  449. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  450. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  451. package/dist/test/unit/handoff-export.test.js +0 -263
  452. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  453. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  454. package/dist/test/unit/logging.test.d.ts +0 -1
  455. package/dist/test/unit/logging.test.js +0 -397
  456. package/dist/test/unit/path-guard.test.d.ts +0 -1
  457. package/dist/test/unit/path-guard.test.js +0 -133
  458. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  459. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  460. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  461. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  462. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  463. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  464. package/dist/test/unit/release-gate.test.d.ts +0 -1
  465. package/dist/test/unit/release-gate.test.js +0 -242
  466. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  467. package/dist/test/unit/run-task-loop.test.js +0 -380
  468. package/dist/test/unit/safe-status.test.d.ts +0 -1
  469. package/dist/test/unit/safe-status.test.js +0 -165
  470. package/dist/test/unit/safe-views.test.d.ts +0 -1
  471. package/dist/test/unit/safe-views.test.js +0 -171
  472. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  473. package/dist/test/unit/schema-drift-check.test.js +0 -176
  474. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  475. package/dist/test/unit/sensitive-guard.test.js +0 -104
  476. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  477. package/dist/test/unit/spec-kit-import.test.js +0 -341
  478. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  479. package/dist/test/unit/subgoal-sync.test.js +0 -183
  480. package/dist/test/unit/sync-file.test.d.ts +0 -1
  481. package/dist/test/unit/sync-file.test.js +0 -154
  482. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  483. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  484. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  485. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  486. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  487. package/dist/test/unit/toolSearch.test.js +0 -571
  488. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  489. package/dist/test/unit/wait-for-task.test.js +0 -144
  490. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  491. package/dist/test/unit/watcher-status.test.js +0 -169
  492. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  493. package/dist/test/unit/worktree-manager.test.js +0 -176
  494. package/dist/tools/applyPatch.js +0 -41
  495. package/dist/tools/cancelTask.d.ts +0 -30
  496. package/dist/tools/cancelTask.js +0 -64
  497. package/dist/tools/finalizeDirectSession.js +0 -84
  498. package/dist/tools/goalSubgoalTask.js +0 -110
  499. package/dist/tools/killTask.d.ts +0 -15
  500. package/dist/tools/listAgents.d.ts +0 -12
  501. package/dist/tools/listAgents.js +0 -42
  502. package/dist/tools/reconcileTasks.js +0 -352
  503. package/dist/tools/retryTask.js +0 -45
  504. package/dist/tools/runVerification.js +0 -36
  505. package/dist/tools/syncFile.js +0 -65
  506. package/docs/control-center/control-center-daily-driver.md +0 -211
  507. package/docs/control-center/control-center-mvp.md +0 -205
  508. package/docs/control-center/control-center-phase2.md +0 -159
  509. package/docs/execution-plan-2026-07-09.md +0 -315
  510. package/docs/release-v0.6.0.md +0 -71
  511. package/docs/release-v0.6.1.md +0 -75
  512. package/docs/release-v0.6.4.md +0 -45
  513. package/docs/roadmap-execution-and-acceptance.md +0 -365
  514. package/src/test/unit/acceptance-engine.test.ts +0 -261
  515. package/src/test/unit/agent-router.test.ts +0 -342
  516. package/src/test/unit/android-doctor.test.ts +0 -158
  517. package/src/test/unit/apply-patch.test.ts +0 -293
  518. package/src/test/unit/audit-checks.test.ts +0 -567
  519. package/src/test/unit/chinese-path.test.ts +0 -106
  520. package/src/test/unit/command-guard.test.ts +0 -221
  521. package/src/test/unit/create-task.test.ts +0 -255
  522. package/src/test/unit/diagnose-task.test.ts +0 -544
  523. package/src/test/unit/direct-guards.test.ts +0 -467
  524. package/src/test/unit/discovery-token-store.test.ts +0 -181
  525. package/src/test/unit/evidence-pack.test.ts +0 -251
  526. package/src/test/unit/get-task-status.test.ts +0 -203
  527. package/src/test/unit/get-task-summary.test.ts +0 -173
  528. package/src/test/unit/goal-graph.test.ts +0 -347
  529. package/src/test/unit/goal-progress.test.ts +0 -538
  530. package/src/test/unit/goal-report.test.ts +0 -189
  531. package/src/test/unit/goal-status.test.ts +0 -270
  532. package/src/test/unit/goal-store.test.ts +0 -318
  533. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  534. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  535. package/src/test/unit/handoff-export.test.ts +0 -295
  536. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  537. package/src/test/unit/logging.test.ts +0 -497
  538. package/src/test/unit/path-guard.test.ts +0 -180
  539. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  540. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  541. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  542. package/src/test/unit/release-gate.test.ts +0 -314
  543. package/src/test/unit/run-task-loop.test.ts +0 -425
  544. package/src/test/unit/safe-status.test.ts +0 -187
  545. package/src/test/unit/safe-views.test.ts +0 -184
  546. package/src/test/unit/schema-drift-check.test.ts +0 -260
  547. package/src/test/unit/sensitive-guard.test.ts +0 -124
  548. package/src/test/unit/spec-kit-import.test.ts +0 -429
  549. package/src/test/unit/subgoal-sync.test.ts +0 -236
  550. package/src/test/unit/sync-file.test.ts +0 -231
  551. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  552. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  553. package/src/test/unit/toolSearch.test.ts +0 -756
  554. package/src/test/unit/wait-for-task.test.ts +0 -176
  555. package/src/test/unit/watcher-status.test.ts +0 -190
  556. package/src/test/unit/worktree-manager.test.ts +0 -247
  557. package/src/tools/cancelTask.ts +0 -70
  558. package/src/tools/listAgents.ts +0 -54
  559. package/src/tools/retryTask.ts +0 -53
  560. package/src/tools/syncFile.ts +0 -122
  561. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  562. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  563. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  564. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  565. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  566. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  567. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  568. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  569. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  570. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  571. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  572. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  573. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  574. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  575. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  576. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  577. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  578. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  579. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  580. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  581. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  582. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  583. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  584. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  585. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  586. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  587. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  588. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  589. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  590. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  591. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
@@ -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
  }