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
@@ -8,13 +8,29 @@
8
8
  * launching, so the non-interactive web UI never deadlocks on a missing
9
9
  * dependency. All endpoints are POST routes gated by the control token.
10
10
  */
11
- import { existsSync } from "node:fs";
11
+ import { existsSync, readFileSync, statSync } from "node:fs";
12
12
  import { delimiter, extname, join } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
  import { spawn } from "node:child_process";
15
15
  import { type ServerResponse } from "node:http";
16
+ import {
17
+ buildChildEnvironment,
18
+ redactProcessOutput,
19
+ resolveTrustedExecutable,
20
+ sanitizeTrustedPath,
21
+ } from "../../runner/processSecurity.js";
16
22
  import { recordEvent } from "../runtime.js";
17
- import { DEFAULT_TUNNEL_CLIENT_EXE, errorMessage, getRuntimeRoot, manageScriptPath, projectRoot, sendJson } from "../shared.js";
23
+ import { launchFileManager } from "../fileManager.js";
24
+ import {
25
+ config,
26
+ CORE_BASE_URL,
27
+ DIRECT_BASE_URL,
28
+ errorMessage,
29
+ getRuntimeRoot,
30
+ manageScriptPath,
31
+ projectRoot,
32
+ sendJson,
33
+ } from "../shared.js";
18
34
 
19
35
  type ControlMode = "core" | "direct";
20
36
 
@@ -24,14 +40,32 @@ interface ManageResult {
24
40
  stderr: string;
25
41
  }
26
42
 
43
+ const MAX_MANAGE_OUTPUT_CHARS = 128 * 1024;
44
+ const MANAGE_ALLOWED_ENVIRONMENT = [
45
+ "PATCHWARDEN_CONFIG",
46
+ "PATCHWARDEN_TUNNEL_ID",
47
+ "PATCHWARDEN_CREDENTIAL_PATH",
48
+ "PATCHWARDEN_TUNNEL_CLIENT_EXE",
49
+ "TUNNEL_CLIENT_EXE",
50
+ "OPENCODE_BIN_DIR",
51
+ "HTTP_PROXY",
52
+ "HTTPS_PROXY",
53
+ "ALL_PROXY",
54
+ "NO_PROXY",
55
+ ] as const;
56
+
27
57
  function runManageAction(action: string, mode: string): Promise<ManageResult> {
28
58
  return new Promise((resolveP, rejectP) => {
29
59
  let child;
60
+ let exactValues: string[] = [];
30
61
  try {
62
+ const env = buildManageEnvironment();
63
+ exactValues = manageSensitiveValues(env);
64
+ const command = resolveTrustedExecutable("powershell.exe", projectRoot, { pathValue: env.PATH });
31
65
  child = spawn(
32
- "powershell.exe",
66
+ command,
33
67
  ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", manageScriptPath, action, mode, "-Background"],
34
- { cwd: projectRoot, windowsHide: true }
68
+ { cwd: projectRoot, windowsHide: true, env }
35
69
  );
36
70
  } catch (err) {
37
71
  rejectP(err);
@@ -47,10 +81,10 @@ function runManageAction(action: string, mode: string): Promise<ManageResult> {
47
81
  rejectP(new Error(`manage-patchwarden.ps1 timed out after 60s (action=${action}, mode=${mode})`));
48
82
  }, 60_000);
49
83
  child.stdout.on("data", (d: Buffer) => {
50
- stdout += d.toString("utf-8");
84
+ stdout = appendBoundedManageOutput(stdout, d.toString("utf-8"));
51
85
  });
52
86
  child.stderr.on("data", (d: Buffer) => {
53
- stderr += d.toString("utf-8");
87
+ stderr = appendBoundedManageOutput(stderr, d.toString("utf-8"));
54
88
  });
55
89
  child.on("error", (err) => {
56
90
  if (settled) return;
@@ -62,14 +96,31 @@ function runManageAction(action: string, mode: string): Promise<ManageResult> {
62
96
  if (settled) return;
63
97
  settled = true;
64
98
  clearTimeout(timer);
65
- resolveP({ exitCode: code ?? -1, stdout, stderr });
99
+ resolveP({
100
+ exitCode: code ?? -1,
101
+ stdout: redactProcessOutput(stdout, exactValues),
102
+ stderr: redactProcessOutput(stderr, exactValues),
103
+ });
66
104
  });
67
105
  });
68
106
  }
69
107
 
70
- function selectedControlModes(mode: string): ControlMode[] {
71
- if (mode === "core" || mode === "direct") return [mode];
72
- return ["core", "direct"];
108
+ export function resolveManageProfiles(mode: string, action: string, directEnabled: boolean): { selected: ControlMode[]; skipped: ControlMode[] } {
109
+ if (mode === "core" || mode === "direct") return { selected: [mode], skipped: [] };
110
+ if ((action === "start" || action === "restart") && !directEnabled) return { selected: ["core"], skipped: ["direct"] };
111
+ return { selected: ["core", "direct"], skipped: [] };
112
+ }
113
+
114
+ function selectedControlModes(mode: string, action = "status"): ControlMode[] {
115
+ return resolveManageProfiles(mode, action, config.enableDirectProfile === true).selected;
116
+ }
117
+
118
+ function skippedControlModes(mode: string, action: string): ControlMode[] {
119
+ return resolveManageProfiles(mode, action, config.enableDirectProfile === true).skipped;
120
+ }
121
+
122
+ function effectiveManageMode(modes: ControlMode[]): string {
123
+ return modes.length === 1 ? modes[0] : "all";
73
124
  }
74
125
 
75
126
  function launcherPathForMode(mode: ControlMode): string {
@@ -78,7 +129,7 @@ function launcherPathForMode(mode: ControlMode): string {
78
129
  }
79
130
 
80
131
  function findExecutableOnPath(fileName: string): string | null {
81
- const pathValue = process.env.PATH || "";
132
+ const pathValue = sanitizeTrustedPath(process.env.PATH || "", projectRoot);
82
133
  const extensions =
83
134
  process.platform === "win32"
84
135
  ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
@@ -86,11 +137,11 @@ function findExecutableOnPath(fileName: string): string | null {
86
137
  for (const entry of pathValue.split(delimiter)) {
87
138
  if (!entry) continue;
88
139
  const direct = join(entry, fileName);
89
- if (existsSync(direct)) return direct;
140
+ if (isRegularFile(direct)) return direct;
90
141
  if (process.platform === "win32" && !extname(fileName)) {
91
142
  for (const ext of extensions) {
92
143
  const candidate = join(entry, fileName + ext.toLowerCase());
93
- if (existsSync(candidate)) return candidate;
144
+ if (isRegularFile(candidate)) return candidate;
94
145
  }
95
146
  }
96
147
  }
@@ -99,70 +150,296 @@ function findExecutableOnPath(fileName: string): string | null {
99
150
 
100
151
  function findTunnelClientExecutable(): string | null {
101
152
  if (process.env.PATCHWARDEN_CONTROL_FORCE_MISSING_TUNNEL_CLIENT === "1") return null;
102
- const explicit = process.env.TUNNEL_CLIENT_EXE || process.env.PATCHWARDEN_TUNNEL_CLIENT_EXE;
103
- if (explicit && existsSync(explicit)) return explicit;
153
+ const configured = config.tunnelClientPath;
154
+ if (configured && isRegularFile(configured)) return configured;
155
+ const explicit = process.env.PATCHWARDEN_TUNNEL_CLIENT_EXE || process.env.TUNNEL_CLIENT_EXE;
156
+ if (explicit && isRegularFile(explicit)) return explicit;
104
157
  const fromPath = findExecutableOnPath("tunnel-client.exe") ?? findExecutableOnPath("tunnel-client");
105
158
  if (fromPath) return fromPath;
106
159
 
107
160
  const candidates = [
108
- DEFAULT_TUNNEL_CLIENT_EXE,
109
- process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "patchwarden", "tunnel-client.exe") : null,
161
+ process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "PatchWarden", "tunnel-client.exe") : null,
110
162
  process.env.APPDATA ? join(process.env.APPDATA, "tunnel-client", "tunnel-client.exe") : null,
111
163
  join(homedir(), "tunnel-client", "tunnel-client.exe"),
112
164
  ].filter((v): v is string => typeof v === "string" && v.length > 0);
113
165
  for (const candidate of candidates) {
114
- if (existsSync(candidate)) return candidate;
166
+ if (isRegularFile(candidate)) return candidate;
115
167
  }
116
168
  return null;
117
169
  }
118
170
 
171
+ export function buildManageEnvironment(): NodeJS.ProcessEnv {
172
+ const agentEnvironmentNames = [...new Set(Object.values(config.agents)
173
+ .flatMap((agent) => agent.envAllowlist ?? []))];
174
+ const env = buildChildEnvironment({
175
+ cwd: projectRoot,
176
+ allowedNames: [...MANAGE_ALLOWED_ENVIRONMENT, ...agentEnvironmentNames],
177
+ blockedNames: [config.http?.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN"],
178
+ });
179
+ const lifecycleNames = new Set(MANAGE_ALLOWED_ENVIRONMENT.map((name) => name.toUpperCase()));
180
+ const providerOnlyNames = agentEnvironmentNames.filter(
181
+ (name) => !lifecycleNames.has(name.toUpperCase()),
182
+ );
183
+ env.PATCHWARDEN_AGENT_ENV_ALLOWLIST = providerOnlyNames.join(";");
184
+ const tunnelClient = findTunnelClientExecutable();
185
+ if (tunnelClient) {
186
+ env.PATCHWARDEN_TUNNEL_CLIENT_EXE = tunnelClient;
187
+ env.TUNNEL_CLIENT_EXE = tunnelClient;
188
+ }
189
+ const proxy = config.tunnelProxy;
190
+ const core = proxy?.core || { mode: "environment" as const };
191
+ const direct = proxy?.scope === "separate" ? proxy.direct : core;
192
+ env.PATCHWARDEN_TUNNEL_PROXY_MODE = core.mode;
193
+ env.PATCHWARDEN_TUNNEL_PROXY_URL = core.mode === "manual" ? core.url || "" : "";
194
+ env.PATCHWARDEN_DIRECT_PROXY_MODE = direct.mode;
195
+ env.PATCHWARDEN_DIRECT_PROXY_URL = direct.mode === "manual" ? direct.url || "" : "";
196
+ return env;
197
+ }
198
+
119
199
  // Re-exported so the status route can surface tunnel-client availability in
120
200
  // /api/status without duplicating the discovery logic.
121
201
  export { findTunnelClientExecutable };
122
202
 
203
+ function profilePathForMode(mode: ControlMode): string | null {
204
+ if (!process.env.APPDATA) return null;
205
+ return join(process.env.APPDATA, "tunnel-client", mode === "direct" ? "patchwarden-direct.yaml" : "patchwarden.yaml");
206
+ }
207
+
208
+ function credentialPath(): string | null {
209
+ return process.env.APPDATA ? join(process.env.APPDATA, "patchwarden", "control-plane-api-key.dpapi") : null;
210
+ }
211
+
123
212
  function preflightManageAction(action: string, mode: string): { status: number; body: Record<string, unknown> } | null {
124
213
  if (action !== "start" && action !== "restart") return null;
125
214
 
126
- const missingLaunchers = selectedControlModes(mode)
215
+ const selectedModes = selectedControlModes(mode, action);
216
+ const missingLaunchers = selectedModes
127
217
  .map((m) => ({ mode: m, path: launcherPathForMode(m) }))
128
218
  .filter((entry) => !existsSync(entry.path));
129
219
  const tunnelClient = findTunnelClientExecutable();
220
+ const missingProfiles = selectedModes
221
+ .map((m) => ({ mode: m, path: profilePathForMode(m) }))
222
+ .filter((entry) => !entry.path || !existsSync(entry.path));
223
+ const savedCredential = credentialPath();
130
224
  const missing: string[] = [];
131
225
  if (!tunnelClient) missing.push("tunnel-client.exe");
132
226
  for (const entry of missingLaunchers) missing.push(`${entry.mode} launcher`);
227
+ for (const entry of missingProfiles) missing.push(`${entry.mode} profile`);
228
+ if (!savedCredential || !existsSync(savedCredential)) missing.push("tunnel runtime credential");
133
229
 
134
230
  if (missing.length === 0) return null;
135
231
 
232
+ const reasonCode = !tunnelClient
233
+ ? "tunnel_client_missing"
234
+ : missingLaunchers.length > 0
235
+ ? "launcher_missing"
236
+ : missingProfiles.length > 0
237
+ ? "tunnel_profile_missing"
238
+ : "tunnel_credential_missing";
239
+
136
240
  return {
137
241
  status: 409,
138
242
  body: {
139
243
  ok: false,
140
244
  action,
141
245
  mode,
246
+ reason_code: reasonCode,
142
247
  error:
143
248
  "Control Center preflight failed. Start/restart from the Web UI is non-interactive, so required runtime dependencies must be available before launching.",
144
249
  missing,
145
250
  next_steps: [
146
- "Install tunnel-client.exe or set TUNNEL_CLIENT_EXE / PATCHWARDEN_TUNNEL_CLIENT_EXE to its full path.",
147
- `Default checked path: ${DEFAULT_TUNNEL_CLIENT_EXE}`,
148
- "Verify the launcher files under scripts/launchers are present.",
149
- "Then retry from PatchWarden Control Center or PatchWarden-Control-Tray.cmd.",
251
+ "在桌面设置中自动检测或选择 tunnel-client.exe,也可设置 PATCHWARDEN_TUNNEL_CLIENT_EXE",
252
+ "在桌面设置中配置 Tunnel ID 和专用 runtime API key,并完成 doctor 验证。",
253
+ "确认 scripts/launchers 下的启动器存在,然后重试。",
150
254
  ],
151
255
  },
152
256
  };
153
257
  }
154
258
 
259
+ function preflightDirectProfile(action: string, mode: string): { status: number; body: Record<string, unknown> } | null {
260
+ if ((action !== "start" && action !== "restart") || mode !== "direct") return null;
261
+ if (config.enableDirectProfile === true) return null;
262
+ return {
263
+ status: 409,
264
+ body: {
265
+ ok: false,
266
+ action,
267
+ mode,
268
+ reason_code: "direct_profile_disabled",
269
+ error: "Direct profile 尚未启用。请先在桌面设置中启用 Direct,再启动对应隧道。",
270
+ next_steps: ["打开 设置 -> MCP 与隧道", "启用 Direct profile 并保存", "重新启动 Direct"],
271
+ },
272
+ };
273
+ }
274
+
275
+ function readRuntimeState(mode: ControlMode, fileName: string): Record<string, unknown> | null {
276
+ try {
277
+ return JSON.parse(readFileSync(join(getRuntimeRoot(mode === "direct"), fileName), "utf8").replace(/^\uFEFF/, ""));
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
283
+ function readRuntimeLog(mode: ControlMode, fileName: string): string {
284
+ try {
285
+ const value = readFileSync(join(getRuntimeRoot(mode === "direct"), fileName), "utf8");
286
+ return value.length > 64_000 ? value.slice(-64_000) : value;
287
+ } catch {
288
+ return "";
289
+ }
290
+ }
291
+
292
+ export function classifySupervisorFailure(diagnosticText: string): string {
293
+ if (/unsupported_country_region_territory|unsupported.{0,30}(country|region|territory)/i.test(diagnosticText)) return "unsupported_region";
294
+ if (/\b(401|403)\b|unauthori[sz]ed|invalid.{0,30}(api.?key|credential)|api.?key.{0,30}(invalid|missing)/i.test(diagnosticText)) return "auth_failed";
295
+ if (/proxy|ECONNREFUSED|ENETUNREACH|ETIMEDOUT|could not resolve|name resolution|connection (failed|refused)/i.test(diagnosticText)) return "proxy_unreachable";
296
+ if (/tool manifest preflight|tool_manifest_(check_failed|invalid)|mcp-manifest-check/i.test(diagnosticText)) return "tool_manifest_check_failed";
297
+ if (/spawn EPERM|access (is )?denied|permission denied/i.test(diagnosticText)) return "supervisor_permission_denied";
298
+ if (/watcher.{0,40}(failed|unhealthy|missing|exited)/i.test(diagnosticText)) return "watcher_unhealthy";
299
+ if (/profile.{0,30}(missing|invalid)|config.{0,30}(missing|invalid)|launcher.{0,30}(missing|not found)/i.test(diagnosticText)) return "config_error";
300
+ return "supervisor_exited";
301
+ }
302
+
303
+ function processIsAlive(pid: unknown): boolean {
304
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
305
+ try {
306
+ process.kill(pid, 0);
307
+ return true;
308
+ } catch (err) {
309
+ return (err as NodeJS.ErrnoException).code === "EPERM";
310
+ }
311
+ }
312
+
313
+ function inspectSupervisor(mode: ControlMode, startedAt: number): { terminal: boolean; reasonCode: string | null } {
314
+ const state = readRuntimeState(mode, "supervisor-status.json");
315
+ const checkedAt = typeof state?.checked_at === "string" ? Date.parse(state.checked_at) : 0;
316
+ const fresh = Number.isFinite(checkedAt) && checkedAt >= startedAt - 2000;
317
+ if (!fresh) return { terminal: false, reasonCode: null };
318
+ const explicitlyFailed = state?.status === "failed";
319
+ if (!explicitlyFailed && processIsAlive(state?.pid)) return { terminal: false, reasonCode: null };
320
+ const diagnostics = `${readRuntimeLog(mode, "supervisor.stderr.log")}\n${readRuntimeLog(mode, "supervisor.stdout.log")}`;
321
+ const recordedReason = typeof state?.reason_code === "string" ? state.reason_code : null;
322
+ return { terminal: true, reasonCode: recordedReason || classifySupervisorFailure(diagnostics) };
323
+ }
324
+
325
+ async function probeReady(baseUrl: string): Promise<boolean> {
326
+ const controller = new AbortController();
327
+ const timer = setTimeout(() => controller.abort(), 1500);
328
+ try {
329
+ const response = await fetch(`${baseUrl}/readyz`, { signal: controller.signal, cache: "no-store" });
330
+ return response.ok;
331
+ } catch {
332
+ return false;
333
+ } finally {
334
+ clearTimeout(timer);
335
+ }
336
+ }
337
+
338
+ async function inspectStartup(mode: ControlMode, startedAt: number): Promise<Record<string, unknown>> {
339
+ const tunnel = readRuntimeState(mode, "tunnel-status.json");
340
+ const checkedAt = typeof tunnel?.checked_at === "string" ? Date.parse(tunnel.checked_at) : 0;
341
+ const fresh = Number.isFinite(checkedAt) && checkedAt >= startedAt - 2000;
342
+ const endpointReady = await probeReady(mode === "direct" ? DIRECT_BASE_URL : CORE_BASE_URL);
343
+ const watcher = mode === "core" ? readRuntimeState(mode, "watcher-status.json") : null;
344
+ const watcherReady = mode === "direct" || watcher?.status === "healthy";
345
+ const ready = endpointReady && tunnel?.ready !== false && watcherReady;
346
+ const terminalStatuses = new Set(["failed", "error", "stopped", "restart_limit_reached"]);
347
+ const tunnelTerminal = fresh && terminalStatuses.has(String(tunnel?.status || "")) && tunnel?.reason_code !== "stopped_by_manager";
348
+ const supervisor = inspectSupervisor(mode, startedAt);
349
+ const terminal = tunnelTerminal || supervisor.terminal;
350
+ const reasonCode = tunnelTerminal
351
+ ? (typeof tunnel?.reason_code === "string" ? tunnel.reason_code : "startup_failed")
352
+ : supervisor.reasonCode;
353
+ return {
354
+ mode,
355
+ ready,
356
+ endpoint_ready: endpointReady,
357
+ watcher_ready: watcherReady,
358
+ status: tunnel?.status || "starting",
359
+ reason_code: reasonCode,
360
+ error: tunnel?.last_error || null,
361
+ terminal,
362
+ };
363
+ }
364
+
365
+ async function waitForStartup(modes: ControlMode[], startedAt: number): Promise<{ ok: boolean; profiles: Record<string, unknown>[]; reasonCode?: string }> {
366
+ const configured = Number(process.env.PATCHWARDEN_CONTROL_START_TIMEOUT_MS || "30000");
367
+ const timeoutMs = Number.isFinite(configured) ? Math.min(Math.max(configured, 1000), 120000) : 30000;
368
+ const deadline = Date.now() + timeoutMs;
369
+ let profiles: Record<string, unknown>[] = [];
370
+ while (Date.now() < deadline) {
371
+ profiles = await Promise.all(modes.map((mode) => inspectStartup(mode, startedAt)));
372
+ if (profiles.every((profile) => profile.ready === true)) return { ok: true, profiles };
373
+ const failed = profiles.find((profile) => profile.terminal === true);
374
+ if (failed) {
375
+ const reasonCode = typeof failed.reason_code === "string" ? failed.reason_code : "supervisor_exited";
376
+ return { ok: false, profiles, reasonCode };
377
+ }
378
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
379
+ }
380
+ profiles = await Promise.all(modes.map((mode) => inspectStartup(mode, startedAt)));
381
+ return { ok: false, profiles, reasonCode: "startup_timeout" };
382
+ }
383
+
155
384
  export async function handleManageAction(res: ServerResponse, action: string, mode: string): Promise<void> {
156
385
  try {
386
+ const directPreflight = preflightDirectProfile(action, mode);
387
+ if (directPreflight) {
388
+ recordEvent("manage." + mode + "." + action + ".profile_disabled", { reason_code: "direct_profile_disabled" });
389
+ sendJson(res, directPreflight.status, directPreflight.body);
390
+ return;
391
+ }
392
+ const selectedModes = selectedControlModes(mode, action);
393
+ const skippedModes = skippedControlModes(mode, action);
394
+ const managedMode = effectiveManageMode(selectedModes);
157
395
  const preflight = preflightManageAction(action, mode);
158
396
  if (preflight) {
159
397
  recordEvent("manage." + mode + "." + action + ".preflight_failed", {
160
398
  missing: preflight.body.missing,
161
399
  });
162
- sendJson(res, preflight.status, preflight.body);
400
+ sendJson(res, preflight.status, {
401
+ ...preflight.body,
402
+ started_profiles: [],
403
+ skipped_profiles: skippedModes,
404
+ });
163
405
  return;
164
406
  }
165
- const result = await runManageAction(action, mode);
407
+ const startedAt = Date.now();
408
+ const result = await runManageAction(action, managedMode);
409
+ if (result.exitCode !== 0) {
410
+ recordEvent("manage." + mode + "." + action, { exit_code: result.exitCode, ok: false });
411
+ sendJson(res, 500, {
412
+ ok: false,
413
+ action,
414
+ mode,
415
+ started_profiles: [],
416
+ skipped_profiles: skippedModes,
417
+ reason_code: "manager_failed",
418
+ error: result.stderr.trim() || result.stdout.trim() || `manage-patchwarden.ps1 exited with code ${result.exitCode}`,
419
+ });
420
+ return;
421
+ }
422
+ if (action === "start" || action === "restart") {
423
+ const startup = await waitForStartup(selectedModes, startedAt);
424
+ if (!startup.ok) {
425
+ const timeout = startup.reasonCode === "startup_timeout";
426
+ recordEvent("manage." + mode + "." + action + ".not_ready", { reason_code: startup.reasonCode, profiles: startup.profiles });
427
+ sendJson(res, timeout ? 504 : 502, {
428
+ ok: false,
429
+ action,
430
+ mode,
431
+ started_profiles: selectedModes,
432
+ skipped_profiles: skippedModes,
433
+ reason_code: startup.reasonCode,
434
+ error: timeout
435
+ ? "启动命令已执行,但服务未在等待时间内通过就绪检查。"
436
+ : "隧道 supervisor 在服务就绪前退出。",
437
+ profiles: startup.profiles,
438
+ next_steps: ["检查设置中的 tunnel-client 与代理", "运行健康检查", "查看本次 Core / Direct 日志"],
439
+ });
440
+ return;
441
+ }
442
+ }
166
443
  recordEvent("manage." + mode + "." + action, {
167
444
  exit_code: result.exitCode,
168
445
  ok: result.exitCode === 0,
@@ -172,6 +449,10 @@ export async function handleManageAction(res: ServerResponse, action: string, mo
172
449
  exitCode: result.exitCode,
173
450
  stdout: result.stdout,
174
451
  stderr: result.stderr,
452
+ verified: action === "start" || action === "restart",
453
+ started_profiles: action === "start" || action === "restart" ? selectedModes : [],
454
+ skipped_profiles: skippedModes,
455
+ reason_code: skippedModes.length > 0 ? "direct_profile_disabled" : null,
175
456
  });
176
457
  } catch (err) {
177
458
  recordEvent("manage." + mode + "." + action + ".failed", { error: errorMessage(err) });
@@ -182,23 +463,43 @@ export async function handleManageAction(res: ServerResponse, action: string, mo
182
463
  export function handleOpenLogsFolder(res: ServerResponse): void {
183
464
  try {
184
465
  const target = getRuntimeRoot(false);
185
- let cmd: string;
186
- if (process.platform === "win32") {
187
- cmd = "explorer.exe";
188
- } else if (process.platform === "darwin") {
189
- cmd = "open";
190
- } else {
191
- cmd = "xdg-open";
192
- }
193
- try {
194
- const child = spawn(cmd, [target], { detached: true, stdio: "ignore" });
195
- child.on("error", () => { /* ignore spawn errors */ });
196
- child.unref();
197
- } catch {
198
- /* ignore */
199
- }
466
+ launchFileManager(target, projectRoot);
200
467
  sendJson(res, 200, { ok: true, path: target });
201
468
  } catch (err) {
202
469
  sendJson(res, 500, { error: errorMessage(err) });
203
470
  }
204
471
  }
472
+
473
+ function appendBoundedManageOutput(current: string, next: string): string {
474
+ if (current.length >= MAX_MANAGE_OUTPUT_CHARS) return current;
475
+ return current + next.slice(0, MAX_MANAGE_OUTPUT_CHARS - current.length);
476
+ }
477
+
478
+ function manageSensitiveValues(env: NodeJS.ProcessEnv): string[] {
479
+ const agentEnvironmentValues = Object.values(config.agents)
480
+ .flatMap((agent) => agent.envAllowlist ?? [])
481
+ .map((name) => findEnvironmentValue(env, name));
482
+ const values = [
483
+ env.HTTP_PROXY,
484
+ env.HTTPS_PROXY,
485
+ env.ALL_PROXY,
486
+ env.PATCHWARDEN_TUNNEL_PROXY_URL,
487
+ env.PATCHWARDEN_DIRECT_PROXY_URL,
488
+ ...agentEnvironmentValues,
489
+ ].filter((value): value is string => typeof value === "string" && Buffer.byteLength(value, "utf-8") >= 8);
490
+ return [...new Set(values)].sort((left, right) => right.length - left.length);
491
+ }
492
+
493
+ function findEnvironmentValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
494
+ if (process.platform !== "win32") return env[name];
495
+ const match = Object.keys(env).find((key) => key.toUpperCase() === name.toUpperCase());
496
+ return match ? env[match] : undefined;
497
+ }
498
+
499
+ function isRegularFile(path: string): boolean {
500
+ try {
501
+ return statSync(path).isFile();
502
+ } catch {
503
+ return false;
504
+ }
505
+ }