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