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