patchwarden 1.1.0 → 1.5.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 (258) hide show
  1. package/README.en.md +83 -8
  2. package/README.md +78 -10
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2001
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/direct/directSessionStore.d.ts +2 -0
  44. package/dist/direct/directVerification.js +7 -0
  45. package/dist/doctor.d.ts +18 -1
  46. package/dist/doctor.js +579 -348
  47. package/dist/goal/goalReport.d.ts +54 -0
  48. package/dist/goal/goalReport.js +204 -0
  49. package/dist/goal/goalStatus.d.ts +6 -0
  50. package/dist/goal/specKitImport.d.ts +63 -0
  51. package/dist/goal/specKitImport.js +220 -0
  52. package/dist/goal/subgoalSync.js +2 -1
  53. package/dist/httpServer.js +15 -12
  54. package/dist/index.js +7 -4
  55. package/dist/logging.d.ts +7 -1
  56. package/dist/logging.js +8 -0
  57. package/dist/policy/projectPolicy.d.ts +55 -0
  58. package/dist/policy/projectPolicy.js +286 -0
  59. package/dist/runner/changeCapture.d.ts +3 -3
  60. package/dist/runner/changeCapture.js +63 -39
  61. package/dist/runner/cli.js +7 -6
  62. package/dist/runner/postTaskCleanup.js +26 -2
  63. package/dist/runner/runTask.js +245 -221
  64. package/dist/runner/simpleProcess.js +4 -4
  65. package/dist/runner/watch.js +17 -14
  66. package/dist/security/contentRedaction.d.ts +6 -0
  67. package/dist/security/contentRedaction.js +22 -0
  68. package/dist/smoke-test.js +261 -255
  69. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  70. package/dist/test/unit/apply-patch.test.js +225 -0
  71. package/dist/test/unit/create-task.test.d.ts +1 -0
  72. package/dist/test/unit/create-task.test.js +197 -0
  73. package/dist/test/unit/direct-guards.test.js +124 -9
  74. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  75. package/dist/test/unit/evidence-pack.test.js +224 -0
  76. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  77. package/dist/test/unit/get-task-status.test.js +174 -0
  78. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  79. package/dist/test/unit/get-task-summary.test.js +146 -0
  80. package/dist/test/unit/goal-report.test.d.ts +1 -0
  81. package/dist/test/unit/goal-report.test.js +159 -0
  82. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  83. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  84. package/dist/test/unit/path-guard.test.js +24 -0
  85. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  86. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  87. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  88. package/dist/test/unit/run-task-loop.test.js +380 -0
  89. package/dist/test/unit/schema-drift-check.test.js +10 -9
  90. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  91. package/dist/test/unit/spec-kit-import.test.js +341 -0
  92. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  93. package/dist/test/unit/wait-for-task.test.js +144 -0
  94. package/dist/tools/auditTask.d.ts +8 -63
  95. package/dist/tools/auditTask.js +12 -8
  96. package/dist/tools/createDirectSession.d.ts +1 -1
  97. package/dist/tools/createDirectSession.js +2 -2
  98. package/dist/tools/createTask.d.ts +2 -2
  99. package/dist/tools/createTask.js +4 -4
  100. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  101. package/dist/tools/dispatch/coreDispatch.js +282 -0
  102. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  103. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  104. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  105. package/dist/tools/dispatch/directDispatch.js +115 -0
  106. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  107. package/dist/tools/dispatch/goalDispatch.js +91 -0
  108. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  109. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  110. package/dist/tools/dispatch/types.d.ts +23 -0
  111. package/dist/tools/dispatch/types.js +15 -0
  112. package/dist/tools/evidencePack.d.ts +45 -0
  113. package/dist/tools/evidencePack.js +375 -0
  114. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  115. package/dist/tools/finalizeDirectSession.js +3 -3
  116. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  117. package/dist/tools/goalSubgoalTask.js +2 -2
  118. package/dist/tools/healthCheck.js +3 -3
  119. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  120. package/dist/tools/recommendAgentForTask.js +56 -0
  121. package/dist/tools/registry.d.ts +3 -3
  122. package/dist/tools/registry.js +333 -402
  123. package/dist/tools/releaseMode.d.ts +50 -0
  124. package/dist/tools/releaseMode.js +370 -0
  125. package/dist/tools/retryTask.d.ts +2 -2
  126. package/dist/tools/retryTask.js +2 -2
  127. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  128. package/dist/tools/runDirectVerificationBundle.js +64 -0
  129. package/dist/tools/runTaskLoop.d.ts +57 -0
  130. package/dist/tools/runTaskLoop.js +417 -0
  131. package/dist/tools/runVerification.d.ts +4 -0
  132. package/dist/tools/runVerification.js +4 -0
  133. package/dist/tools/safeViews.d.ts +8 -2
  134. package/dist/tools/safeViews.js +4 -2
  135. package/dist/tools/taskLineage.d.ts +91 -0
  136. package/dist/tools/taskLineage.js +175 -0
  137. package/dist/tools/toolCatalog.d.ts +2 -2
  138. package/dist/tools/toolCatalog.js +6 -0
  139. package/dist/tools/toolRegistry.js +132 -0
  140. package/dist/version.d.ts +2 -2
  141. package/dist/version.js +2 -2
  142. package/docs/CODE_WIKI.md +983 -0
  143. package/docs/agentseal-integration.md +150 -0
  144. package/docs/architecture.md +63 -0
  145. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  146. package/docs/chatgpt-usage.md +31 -0
  147. package/docs/control-center/README.md +9 -0
  148. package/docs/dashboard-overview.md +86 -0
  149. package/docs/demo.md +8 -0
  150. package/docs/direct-session-workflow.md +98 -0
  151. package/docs/evidence-pack-schema.md +215 -0
  152. package/docs/execution-plan-2026-07-09.md +315 -0
  153. package/docs/lineage-evidence-pack-workflow.md +127 -0
  154. package/docs/mcp-inspector-testing.md +200 -0
  155. package/docs/open-source-application.md +162 -0
  156. package/docs/opencode-worker.md +151 -0
  157. package/docs/openhands-worker.md +181 -0
  158. package/docs/release-evidence.md +72 -0
  159. package/docs/roadmap-execution-and-acceptance.md +365 -0
  160. package/docs/spec-kit-integration.md +131 -0
  161. package/docs/task-safe-review-workflow.md +98 -0
  162. package/docs/threat-model.md +79 -0
  163. package/docs/user-feedback.md +40 -0
  164. package/docs/why-patchwarden.md +110 -0
  165. package/package.json +3 -3
  166. package/scripts/checks/control-center-smoke.js +443 -0
  167. package/scripts/checks/control-smoke.js +2 -2
  168. package/scripts/checks/lifecycle-smoke.js +29 -23
  169. package/scripts/checks/mcp-manifest-check.js +12 -0
  170. package/scripts/checks/mcp-smoke.js +33 -7
  171. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  172. package/scripts/generate-demo-gif.py +320 -0
  173. package/src/assessments/agentAssessor.ts +3 -3
  174. package/src/assessments/assessmentStore.ts +2 -2
  175. package/src/assessments/confirmCli.ts +5 -4
  176. package/src/config.ts +37 -0
  177. package/src/control/middleware/auth.ts +17 -0
  178. package/src/control/middleware/static.ts +71 -0
  179. package/src/control/routes/audit.ts +321 -0
  180. package/src/control/routes/evidence.ts +107 -0
  181. package/src/control/routes/lineage.ts +92 -0
  182. package/src/control/routes/policy.ts +81 -0
  183. package/src/control/routes/process.ts +204 -0
  184. package/src/control/routes/sessions.ts +251 -0
  185. package/src/control/routes/status.ts +325 -0
  186. package/src/control/routes/taskActions.ts +248 -0
  187. package/src/control/routes/tasks.ts +323 -0
  188. package/src/control/routes/workspace.ts +203 -0
  189. package/src/control/runtime.ts +472 -0
  190. package/src/control/server.ts +471 -0
  191. package/src/control/shared.ts +294 -0
  192. package/src/controlCenter.ts +7 -2150
  193. package/src/direct/directGuards.ts +28 -7
  194. package/src/direct/directSessionStore.ts +2 -0
  195. package/src/direct/directVerification.ts +7 -0
  196. package/src/doctor.ts +741 -481
  197. package/src/goal/goalReport.ts +271 -0
  198. package/src/goal/goalStatus.ts +6 -0
  199. package/src/goal/specKitImport.ts +355 -0
  200. package/src/goal/subgoalSync.ts +4 -2
  201. package/src/httpServer.ts +17 -14
  202. package/src/index.ts +7 -4
  203. package/src/logging.ts +10 -1
  204. package/src/policy/projectPolicy.ts +344 -0
  205. package/src/runner/changeCapture.ts +70 -42
  206. package/src/runner/cli.ts +7 -6
  207. package/src/runner/postTaskCleanup.ts +26 -2
  208. package/src/runner/runTask.ts +325 -223
  209. package/src/runner/simpleProcess.ts +4 -4
  210. package/src/runner/watch.ts +17 -14
  211. package/src/security/contentRedaction.ts +29 -0
  212. package/src/smoke-test.ts +254 -252
  213. package/src/test/unit/apply-patch.test.ts +293 -0
  214. package/src/test/unit/create-task.test.ts +255 -0
  215. package/src/test/unit/direct-guards.test.ts +178 -8
  216. package/src/test/unit/evidence-pack.test.ts +251 -0
  217. package/src/test/unit/get-task-status.test.ts +203 -0
  218. package/src/test/unit/get-task-summary.test.ts +173 -0
  219. package/src/test/unit/goal-report.test.ts +189 -0
  220. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  221. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  222. package/src/test/unit/path-guard.test.ts +30 -0
  223. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  224. package/src/test/unit/run-task-loop.test.ts +425 -0
  225. package/src/test/unit/schema-drift-check.test.ts +11 -9
  226. package/src/test/unit/spec-kit-import.test.ts +429 -0
  227. package/src/test/unit/wait-for-task.test.ts +176 -0
  228. package/src/tools/auditTask.ts +99 -59
  229. package/src/tools/createDirectSession.ts +3 -3
  230. package/src/tools/createTask.ts +7 -7
  231. package/src/tools/dispatch/coreDispatch.ts +374 -0
  232. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  233. package/src/tools/dispatch/directDispatch.ts +167 -0
  234. package/src/tools/dispatch/goalDispatch.ts +127 -0
  235. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  236. package/src/tools/dispatch/types.ts +24 -0
  237. package/src/tools/evidencePack.ts +490 -0
  238. package/src/tools/finalizeDirectSession.ts +4 -4
  239. package/src/tools/goalSubgoalTask.ts +2 -2
  240. package/src/tools/healthCheck.ts +3 -3
  241. package/src/tools/listWorkspace.ts +71 -71
  242. package/src/tools/recommendAgentForTask.ts +79 -0
  243. package/src/tools/registry.ts +352 -509
  244. package/src/tools/releaseMode.ts +450 -0
  245. package/src/tools/retryTask.ts +2 -2
  246. package/src/tools/runDirectVerificationBundle.ts +98 -0
  247. package/src/tools/runTaskLoop.ts +526 -0
  248. package/src/tools/runVerification.ts +8 -0
  249. package/src/tools/safeViews.ts +4 -2
  250. package/src/tools/taskLineage.ts +300 -0
  251. package/src/tools/toolCatalog.ts +6 -0
  252. package/src/tools/toolRegistry.ts +132 -0
  253. package/src/version.ts +2 -2
  254. package/ui/pages/audit.html +192 -3
  255. package/ui/pages/dashboard.html +1070 -14
  256. package/ui/pages/direct-sessions.html +505 -53
  257. package/ui/pages/task-detail.html +456 -438
  258. package/ui/pages/tasks.html +598 -61
@@ -0,0 +1,471 @@
1
+ /**
2
+ * PatchWarden Control Center — HTTP server creation, routing, and lifecycle.
3
+ *
4
+ * This module wires together the route handlers (routes/*.ts), middleware
5
+ * (auth/static), and the shared/runtime helpers. It owns the `handleRequest`
6
+ * router that dispatches every GET/POST route, the POST control-token gate,
7
+ * and the server bootstrap/shutdown lifecycle (status file, activity events,
8
+ * SIGINT/SIGTERM).
9
+ *
10
+ * Entry point: `controlCenter.ts` imports `startServer()` from here.
11
+ */
12
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
13
+ import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
14
+ import { logger } from "../logging.js";
15
+ import {
16
+ config,
17
+ controlToken,
18
+ errorMessage,
19
+ host,
20
+ PAGE_ALIASES,
21
+ port,
22
+ readBody,
23
+ resolveTailParam,
24
+ sendJson,
25
+ } from "./shared.js";
26
+ import { recordEvent, removeStatusFile, writeStatusFile } from "./runtime.js";
27
+ import { checkControlToken } from "./middleware/auth.js";
28
+ import { serveFavicon, serveStatic } from "./middleware/static.js";
29
+ import { handleTasks, handleStaleTasks, handleTaskDetail, handleTaskSafeResult, handleTaskSafeAudit, handleTaskSafeTestSummary, handleTaskSafeDiffSummary } from "./routes/tasks.js";
30
+ import { handleReconcile, handleTaskAudit, handleOpenTaskFolder, handleHideStale } from "./routes/taskActions.js";
31
+ import { handleDirectSessions, handleDirectSessionDetail, handleDirectSessionSafeSummary, handleDirectSessionFinalize, handleDirectSessionAudit, handleDirectSessionHide } from "./routes/sessions.js";
32
+ import { handleLineages, handleLineageDetail } from "./routes/lineage.js";
33
+ import { handleEvidencePacks, handleEvidencePackDetail, handleEvidencePackExport } from "./routes/evidence.js";
34
+ import { handleProjectPolicy, handleReleaseStatus } from "./routes/policy.js";
35
+ import { handleWorkspace, handleWorkspaceRepos, handleWorkspaceRepoStatus } from "./routes/workspace.js";
36
+ import { handleManageAction, handleOpenLogsFolder } from "./routes/process.js";
37
+ import { handleStatus, handleControlCenterStatus, handleEvents, handleTunnelUiUrl, handleDiagnostics } from "./routes/status.js";
38
+ import { handleLogs, handleAudit, handleWarnings, type LogCategory } from "./routes/audit.js";
39
+
40
+ // ── Request router ────────────────────────────────────────────────
41
+
42
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
43
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || host}`);
44
+ const pathname = parsedUrl.pathname;
45
+ const method = (req.method || "GET").toUpperCase();
46
+
47
+ // Static routes
48
+ if (method === "GET" && pathname === "/") {
49
+ serveStatic(res, "pages/dashboard.html");
50
+ return;
51
+ }
52
+ const pageAlias = PAGE_ALIASES[pathname];
53
+ if (method === "GET" && pageAlias) {
54
+ serveStatic(res, pageAlias);
55
+ return;
56
+ }
57
+ if (method === "GET" && pathname === "/control-token.json") {
58
+ sendJson(res, 200, { token: controlToken });
59
+ return;
60
+ }
61
+ if (method === "GET" && pathname === "/favicon.ico") {
62
+ serveFavicon(res);
63
+ return;
64
+ }
65
+ if (
66
+ method === "GET" &&
67
+ (pathname === "/colors_and_type.css" ||
68
+ pathname.startsWith("/pages/") ||
69
+ pathname.startsWith("/partials/") ||
70
+ pathname.startsWith("/vendor/"))
71
+ ) {
72
+ serveStatic(res, pathname);
73
+ return;
74
+ }
75
+
76
+ // GET API routes
77
+ if (method === "GET" && pathname === "/api/status") {
78
+ await handleStatus(res);
79
+ return;
80
+ }
81
+ if (method === "GET" && pathname === "/api/tasks") {
82
+ handleTasks(res, {
83
+ repo_path: parsedUrl.searchParams.get("repo_path") || undefined,
84
+ status: parsedUrl.searchParams.get("status") || undefined,
85
+ acceptance_status: parsedUrl.searchParams.get("acceptance_status") || undefined,
86
+ agent: parsedUrl.searchParams.get("agent") || undefined,
87
+ warning_type: parsedUrl.searchParams.get("warning_type") || undefined,
88
+ });
89
+ return;
90
+ }
91
+ if (method === "GET" && pathname === "/api/tasks/stale") {
92
+ handleStaleTasks(res);
93
+ return;
94
+ }
95
+ if (method === "GET" && pathname === "/api/lineages") {
96
+ handleLineages(res);
97
+ return;
98
+ }
99
+ const lineageMatch = pathname.match(/^\/api\/lineages\/([^/]+)$/);
100
+ if (method === "GET" && lineageMatch) {
101
+ let lineageId: string;
102
+ try {
103
+ lineageId = decodeURIComponent(lineageMatch[1]);
104
+ } catch {
105
+ lineageId = lineageMatch[1];
106
+ }
107
+ handleLineageDetail(res, lineageId);
108
+ return;
109
+ }
110
+ if (method === "GET" && pathname === "/api/project-policy") {
111
+ handleProjectPolicy(res, parsedUrl.searchParams.get("repo_path") || ".");
112
+ return;
113
+ }
114
+ if (method === "GET" && pathname === "/api/release/status") {
115
+ handleReleaseStatus(res, parsedUrl.searchParams.get("repo_path") || ".");
116
+ return;
117
+ }
118
+ if (method === "GET" && pathname === "/api/evidence-packs") {
119
+ handleEvidencePacks(res);
120
+ return;
121
+ }
122
+ const evidencePackMatch = pathname.match(/^\/api\/evidence-packs\/([^/]+)$/);
123
+ if (method === "GET" && evidencePackMatch) {
124
+ let lineageId: string;
125
+ try {
126
+ lineageId = decodeURIComponent(evidencePackMatch[1]);
127
+ } catch {
128
+ lineageId = evidencePackMatch[1];
129
+ }
130
+ handleEvidencePackDetail(res, lineageId);
131
+ return;
132
+ }
133
+ const taskMatch = pathname.match(/^\/api\/tasks\/([^/]+)$/);
134
+ if (method === "GET" && taskMatch) {
135
+ let taskId: string;
136
+ try {
137
+ taskId = decodeURIComponent(taskMatch[1]);
138
+ } catch {
139
+ taskId = taskMatch[1];
140
+ }
141
+ if (
142
+ taskId === "." ||
143
+ taskId === ".." ||
144
+ taskId.includes("/") ||
145
+ taskId.includes("\\") ||
146
+ taskId.includes("\0")
147
+ ) {
148
+ sendJson(res, 400, { error: "Invalid task id" });
149
+ return;
150
+ }
151
+ handleTaskDetail(res, taskId);
152
+ return;
153
+ }
154
+ // Safe, bounded views for task artifacts (no full stdout/stderr/diff).
155
+ const taskSafeResultMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-result$/);
156
+ if (method === "GET" && taskSafeResultMatch) {
157
+ let taskId: string;
158
+ try {
159
+ taskId = decodeURIComponent(taskSafeResultMatch[1]);
160
+ } catch {
161
+ taskId = taskSafeResultMatch[1];
162
+ }
163
+ handleTaskSafeResult(res, taskId);
164
+ return;
165
+ }
166
+ const taskSafeAuditMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-audit$/);
167
+ if (method === "GET" && taskSafeAuditMatch) {
168
+ let taskId: string;
169
+ try {
170
+ taskId = decodeURIComponent(taskSafeAuditMatch[1]);
171
+ } catch {
172
+ taskId = taskSafeAuditMatch[1];
173
+ }
174
+ handleTaskSafeAudit(res, taskId);
175
+ return;
176
+ }
177
+ const taskSafeTestSummaryMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-test-summary$/);
178
+ if (method === "GET" && taskSafeTestSummaryMatch) {
179
+ let taskId: string;
180
+ try {
181
+ taskId = decodeURIComponent(taskSafeTestSummaryMatch[1]);
182
+ } catch {
183
+ taskId = taskSafeTestSummaryMatch[1];
184
+ }
185
+ handleTaskSafeTestSummary(res, taskId);
186
+ return;
187
+ }
188
+ const taskSafeDiffSummaryMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-diff-summary$/);
189
+ if (method === "GET" && taskSafeDiffSummaryMatch) {
190
+ let taskId: string;
191
+ try {
192
+ taskId = decodeURIComponent(taskSafeDiffSummaryMatch[1]);
193
+ } catch {
194
+ taskId = taskSafeDiffSummaryMatch[1];
195
+ }
196
+ handleTaskSafeDiffSummary(res, taskId);
197
+ return;
198
+ }
199
+ // Logs: /api/logs/<category>?tail=<100|300|1000>
200
+ const logsMatch = pathname.match(/^\/api\/logs\/([a-z-]+)$/);
201
+ if (method === "GET" && logsMatch) {
202
+ const rawCat = logsMatch[1];
203
+ const category = rawCat === "core" || rawCat === "direct" || rawCat === "watcher" || rawCat === "control-center"
204
+ ? (rawCat as LogCategory)
205
+ : null;
206
+ if (!category) {
207
+ sendJson(res, 404, { error: "Unknown log category" });
208
+ return;
209
+ }
210
+ const tail = resolveTailParam(parsedUrl.searchParams.get("tail"));
211
+ handleLogs(res, category, tail);
212
+ return;
213
+ }
214
+ if (method === "GET" && pathname === "/api/workspace") {
215
+ handleWorkspace(res);
216
+ return;
217
+ }
218
+ if (method === "GET" && pathname === "/api/workspace/repos") {
219
+ handleWorkspaceRepos(res);
220
+ return;
221
+ }
222
+ // On-demand git status for a single repo (path-traversal safe).
223
+ // The repo segment is URL-decoded; traversal is rejected by guardWorkspacePath.
224
+ const workspaceRepoMatch = pathname.match(/^\/api\/workspace\/([^/]+(?:\/[^/]+)*)\/status$/);
225
+ if (method === "GET" && workspaceRepoMatch) {
226
+ let repoParam: string;
227
+ try {
228
+ repoParam = decodeURIComponent(workspaceRepoMatch[1]);
229
+ } catch {
230
+ sendJson(res, 400, { error: "Invalid repo path encoding" });
231
+ return;
232
+ }
233
+ handleWorkspaceRepoStatus(res, repoParam);
234
+ return;
235
+ }
236
+ if (method === "GET" && pathname === "/api/direct-sessions") {
237
+ handleDirectSessions(res);
238
+ return;
239
+ }
240
+ const directSessionSummaryMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/summary$/);
241
+ if (method === "GET" && directSessionSummaryMatch) {
242
+ let sessionId: string;
243
+ try {
244
+ sessionId = decodeURIComponent(directSessionSummaryMatch[1]);
245
+ } catch {
246
+ sessionId = directSessionSummaryMatch[1];
247
+ }
248
+ handleDirectSessionSafeSummary(res, sessionId);
249
+ return;
250
+ }
251
+ const directSessionMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)$/);
252
+ if (method === "GET" && directSessionMatch) {
253
+ let sessionId: string;
254
+ try {
255
+ sessionId = decodeURIComponent(directSessionMatch[1]);
256
+ } catch {
257
+ sessionId = directSessionMatch[1];
258
+ }
259
+ handleDirectSessionDetail(res, sessionId);
260
+ return;
261
+ }
262
+ if (method === "GET" && pathname === "/api/audit") {
263
+ handleAudit(res);
264
+ return;
265
+ }
266
+ if (method === "GET" && pathname === "/api/warnings") {
267
+ handleWarnings(res);
268
+ return;
269
+ }
270
+ if (method === "GET" && pathname === "/api/diagnostics") {
271
+ handleDiagnostics(res);
272
+ return;
273
+ }
274
+ if (method === "GET" && pathname === "/api/tunnel-ui-url") {
275
+ handleTunnelUiUrl(res);
276
+ return;
277
+ }
278
+ if (method === "GET" && pathname === "/api/events") {
279
+ const limitParam = parsedUrl.searchParams.get("limit");
280
+ let limit = 100;
281
+ if (limitParam !== null) {
282
+ const n = parseInt(limitParam, 10);
283
+ if (Number.isFinite(n) && n > 0 && n <= 1000) limit = n;
284
+ }
285
+ handleEvents(res, limit);
286
+ return;
287
+ }
288
+ if (method === "GET" && pathname === "/api/control-center-status") {
289
+ handleControlCenterStatus(res);
290
+ return;
291
+ }
292
+
293
+ // POST API routes (all require control token)
294
+ if (method === "POST") {
295
+ await readBody(req); // drain optional body
296
+ if (!checkControlToken(req)) {
297
+ sendJson(res, 403, { error: "Missing or invalid control token" });
298
+ return;
299
+ }
300
+ if (pathname === "/api/start-all") return handleManageAction(res, "start", "all");
301
+ if (pathname === "/api/stop-all") return handleManageAction(res, "stop", "all");
302
+ if (pathname === "/api/restart-all") return handleManageAction(res, "restart", "all");
303
+ if (pathname === "/api/core/start") return handleManageAction(res, "start", "core");
304
+ if (pathname === "/api/core/stop") return handleManageAction(res, "stop", "core");
305
+ if (pathname === "/api/direct/start") return handleManageAction(res, "start", "direct");
306
+ if (pathname === "/api/direct/stop") return handleManageAction(res, "stop", "direct");
307
+ if (pathname === "/api/open-logs-folder") {
308
+ handleOpenLogsFolder(res);
309
+ return;
310
+ }
311
+ // POST /api/direct-sessions/:sessionId/finalize — finalize a direct session
312
+ // (must be matched BEFORE any generic /api/direct-sessions/:sessionId pattern)
313
+ const finalizeDirectMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/finalize$/);
314
+ if (finalizeDirectMatch) {
315
+ let sessionId: string;
316
+ try {
317
+ sessionId = decodeURIComponent(finalizeDirectMatch[1]);
318
+ } catch {
319
+ sessionId = finalizeDirectMatch[1];
320
+ }
321
+ await handleDirectSessionFinalize(res, sessionId);
322
+ return;
323
+ }
324
+ // POST /api/direct-sessions/:sessionId/audit — audit a direct session
325
+ const auditDirectMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/audit$/);
326
+ if (auditDirectMatch) {
327
+ let sessionId: string;
328
+ try {
329
+ sessionId = decodeURIComponent(auditDirectMatch[1]);
330
+ } catch {
331
+ sessionId = auditDirectMatch[1];
332
+ }
333
+ handleDirectSessionAudit(res, sessionId);
334
+ return;
335
+ }
336
+ // POST /api/direct-sessions/:sessionId/hide — hide a direct session from the list
337
+ const hideDirectMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/hide$/);
338
+ if (hideDirectMatch) {
339
+ let sessionId: string;
340
+ try {
341
+ sessionId = decodeURIComponent(hideDirectMatch[1]);
342
+ } catch {
343
+ sessionId = hideDirectMatch[1];
344
+ }
345
+ handleDirectSessionHide(res, sessionId);
346
+ return;
347
+ }
348
+ // POST /api/tasks/:taskId/reconcile (token already validated above)
349
+ const reconcileMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/reconcile$/);
350
+ if (reconcileMatch) {
351
+ let taskId: string;
352
+ try {
353
+ taskId = decodeURIComponent(reconcileMatch[1]);
354
+ } catch {
355
+ taskId = reconcileMatch[1];
356
+ }
357
+ handleReconcile(res, taskId);
358
+ return;
359
+ }
360
+ // POST /api/tasks/:taskId/audit — run audit_task safely
361
+ const auditMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/audit$/);
362
+ if (auditMatch) {
363
+ let taskId: string;
364
+ try {
365
+ taskId = decodeURIComponent(auditMatch[1]);
366
+ } catch {
367
+ taskId = auditMatch[1];
368
+ }
369
+ handleTaskAudit(res, taskId);
370
+ return;
371
+ }
372
+ // POST /api/tasks/:taskId/open-folder — open task folder in file explorer
373
+ const openFolderMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/open-folder$/);
374
+ if (openFolderMatch) {
375
+ let taskId: string;
376
+ try {
377
+ taskId = decodeURIComponent(openFolderMatch[1]);
378
+ } catch {
379
+ taskId = openFolderMatch[1];
380
+ }
381
+ handleOpenTaskFolder(res, taskId);
382
+ return;
383
+ }
384
+ // POST /api/tasks/:taskId/hide-stale — hide a stale task from the dashboard
385
+ const hideStaleMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/hide-stale$/);
386
+ if (hideStaleMatch) {
387
+ let taskId: string;
388
+ try {
389
+ taskId = decodeURIComponent(hideStaleMatch[1]);
390
+ } catch {
391
+ taskId = hideStaleMatch[1];
392
+ }
393
+ handleHideStale(res, taskId);
394
+ return;
395
+ }
396
+ // POST /api/evidence-packs/:lineageId/export — export an evidence pack for a lineage
397
+ const exportPackMatch = pathname.match(/^\/api\/evidence-packs\/([^/]+)\/export$/);
398
+ if (exportPackMatch) {
399
+ let lineageId: string;
400
+ try {
401
+ lineageId = decodeURIComponent(exportPackMatch[1]);
402
+ } catch {
403
+ lineageId = exportPackMatch[1];
404
+ }
405
+ handleEvidencePackExport(res, lineageId);
406
+ return;
407
+ }
408
+ sendJson(res, 404, { error: "Not found" });
409
+ return;
410
+ }
411
+
412
+ sendJson(res, 404, { error: "Not found" });
413
+ }
414
+
415
+ // ── Server bootstrap ──────────────────────────────────────────────
416
+
417
+ let server: Server | null = null;
418
+
419
+ export function startServer(): Server {
420
+ server = createServer((req, res) => {
421
+ handleRequest(req, res).catch((err) => {
422
+ if (!res.headersSent) {
423
+ sendJson(res, 500, { error: errorMessage(err) });
424
+ } else {
425
+ try { res.end(); } catch { /* ignore */ }
426
+ }
427
+ });
428
+ });
429
+
430
+ server.on("error", (err) => {
431
+ logger.fatal("[control-center] Server error", { error: errorMessage(err) });
432
+ process.exit(1);
433
+ });
434
+
435
+ server.listen(port, host, () => {
436
+ const addr = server!.address();
437
+ const formatted = addr && typeof addr === "object" ? `http://${addr.address}:${addr.port}/` : `http://${host}:${port}/`;
438
+ logger.info(`[control-center] PatchWarden v${PATCHWARDEN_VERSION} (schema epoch ${TOOL_SCHEMA_EPOCH})`);
439
+ logger.info(`[control-center] Workspace: ${config.workspaceRoot}`);
440
+ logger.info(`[control-center] Listening: ${formatted}`);
441
+ logger.info(`[control-center] Bound to 127.0.0.1 only — not exposed to network`);
442
+ // Persist status file so the launcher can detect a running instance and
443
+ // open the browser without spawning a second server.
444
+ writeStatusFile();
445
+ recordEvent("control_center.started", {
446
+ pid: process.pid,
447
+ port,
448
+ url: formatted,
449
+ version: PATCHWARDEN_VERSION,
450
+ });
451
+ });
452
+
453
+ process.on("SIGINT", shutdown);
454
+ process.on("SIGTERM", shutdown);
455
+
456
+ return server;
457
+ }
458
+
459
+ function shutdown(): void {
460
+ logger.info("[control-center] Shutting down...");
461
+ recordEvent("control_center.stopped", { pid: process.pid });
462
+ removeStatusFile();
463
+ if (server) {
464
+ server.close(() => {
465
+ try { process.exit(0); } catch { /* ignore */ }
466
+ });
467
+ }
468
+ setTimeout(() => {
469
+ try { process.exit(0); } catch { /* ignore */ }
470
+ }, 3000).unref();
471
+ }