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,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
  }