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.
- package/README.en.md +43 -2
- package/README.md +43 -2
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directGuards.js +30 -8
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +257 -251
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.js +95 -1
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +6 -0
- package/dist/tools/evidencePack.js +213 -6
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +60 -503
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runTaskLoop.js +4 -4
- package/dist/tools/safeViews.d.ts +2 -2
- package/dist/tools/safeViews.js +2 -2
- package/dist/tools/toolRegistry.js +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +2 -2
- package/scripts/checks/control-center-smoke.js +356 -0
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-smoke.js +3 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directGuards.ts +28 -7
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +252 -250
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +110 -1
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +291 -6
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/registry.ts +68 -628
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runTaskLoop.ts +4 -4
- package/src/tools/safeViews.ts +2 -2
- package/src/tools/toolRegistry.ts +22 -0
- package/src/version.ts +1 -1
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +959 -44
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- 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 };
|