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
@@ -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
+ }