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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — process lifecycle proxy (/api/start-all,
|
|
3
|
+
* /api/stop-all, /api/restart-all, /api/core/*, /api/direct/*).
|
|
4
|
+
*
|
|
5
|
+
* Proxies start/stop/restart actions to scripts/control/manage-patchwarden.ps1
|
|
6
|
+
* and exposes open-logs-folder. Start/restart actions run a preflight that
|
|
7
|
+
* verifies the tunnel-client executable and launcher files exist before
|
|
8
|
+
* launching, so the non-interactive web UI never deadlocks on a missing
|
|
9
|
+
* dependency. All endpoints are POST routes gated by the control token.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { delimiter, extname, join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { recordEvent } from "../runtime.js";
|
|
16
|
+
import { DEFAULT_TUNNEL_CLIENT_EXE, errorMessage, getRuntimeRoot, manageScriptPath, projectRoot, sendJson } from "../shared.js";
|
|
17
|
+
function runManageAction(action, mode) {
|
|
18
|
+
return new Promise((resolveP, rejectP) => {
|
|
19
|
+
let child;
|
|
20
|
+
try {
|
|
21
|
+
child = spawn("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", manageScriptPath, action, mode, "-Background"], { cwd: projectRoot, windowsHide: true });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
rejectP(err);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
let settled = false;
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
if (settled)
|
|
32
|
+
return;
|
|
33
|
+
settled = true;
|
|
34
|
+
try {
|
|
35
|
+
child.kill();
|
|
36
|
+
}
|
|
37
|
+
catch { /* ignore */ }
|
|
38
|
+
rejectP(new Error(`manage-patchwarden.ps1 timed out after 60s (action=${action}, mode=${mode})`));
|
|
39
|
+
}, 60_000);
|
|
40
|
+
child.stdout.on("data", (d) => {
|
|
41
|
+
stdout += d.toString("utf-8");
|
|
42
|
+
});
|
|
43
|
+
child.stderr.on("data", (d) => {
|
|
44
|
+
stderr += d.toString("utf-8");
|
|
45
|
+
});
|
|
46
|
+
child.on("error", (err) => {
|
|
47
|
+
if (settled)
|
|
48
|
+
return;
|
|
49
|
+
settled = true;
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
rejectP(err);
|
|
52
|
+
});
|
|
53
|
+
child.on("close", (code) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
resolveP({ exitCode: code ?? -1, stdout, stderr });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function selectedControlModes(mode) {
|
|
63
|
+
if (mode === "core" || mode === "direct")
|
|
64
|
+
return [mode];
|
|
65
|
+
return ["core", "direct"];
|
|
66
|
+
}
|
|
67
|
+
function launcherPathForMode(mode) {
|
|
68
|
+
const launcherName = mode === "direct" ? "Start-PatchWarden-Direct-Tunnel.cmd" : "Start-PatchWarden-Tunnel.cmd";
|
|
69
|
+
return join(projectRoot, "scripts", "launchers", launcherName);
|
|
70
|
+
}
|
|
71
|
+
function findExecutableOnPath(fileName) {
|
|
72
|
+
const pathValue = process.env.PATH || "";
|
|
73
|
+
const extensions = process.platform === "win32"
|
|
74
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
|
|
75
|
+
: [""];
|
|
76
|
+
for (const entry of pathValue.split(delimiter)) {
|
|
77
|
+
if (!entry)
|
|
78
|
+
continue;
|
|
79
|
+
const direct = join(entry, fileName);
|
|
80
|
+
if (existsSync(direct))
|
|
81
|
+
return direct;
|
|
82
|
+
if (process.platform === "win32" && !extname(fileName)) {
|
|
83
|
+
for (const ext of extensions) {
|
|
84
|
+
const candidate = join(entry, fileName + ext.toLowerCase());
|
|
85
|
+
if (existsSync(candidate))
|
|
86
|
+
return candidate;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function findTunnelClientExecutable() {
|
|
93
|
+
if (process.env.PATCHWARDEN_CONTROL_FORCE_MISSING_TUNNEL_CLIENT === "1")
|
|
94
|
+
return null;
|
|
95
|
+
const explicit = process.env.TUNNEL_CLIENT_EXE || process.env.PATCHWARDEN_TUNNEL_CLIENT_EXE;
|
|
96
|
+
if (explicit && existsSync(explicit))
|
|
97
|
+
return explicit;
|
|
98
|
+
const fromPath = findExecutableOnPath("tunnel-client.exe") ?? findExecutableOnPath("tunnel-client");
|
|
99
|
+
if (fromPath)
|
|
100
|
+
return fromPath;
|
|
101
|
+
const candidates = [
|
|
102
|
+
DEFAULT_TUNNEL_CLIENT_EXE,
|
|
103
|
+
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "patchwarden", "tunnel-client.exe") : null,
|
|
104
|
+
process.env.APPDATA ? join(process.env.APPDATA, "tunnel-client", "tunnel-client.exe") : null,
|
|
105
|
+
join(homedir(), "tunnel-client", "tunnel-client.exe"),
|
|
106
|
+
].filter((v) => typeof v === "string" && v.length > 0);
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (existsSync(candidate))
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// Re-exported so the status route can surface tunnel-client availability in
|
|
114
|
+
// /api/status without duplicating the discovery logic.
|
|
115
|
+
export { findTunnelClientExecutable };
|
|
116
|
+
function preflightManageAction(action, mode) {
|
|
117
|
+
if (action !== "start" && action !== "restart")
|
|
118
|
+
return null;
|
|
119
|
+
const missingLaunchers = selectedControlModes(mode)
|
|
120
|
+
.map((m) => ({ mode: m, path: launcherPathForMode(m) }))
|
|
121
|
+
.filter((entry) => !existsSync(entry.path));
|
|
122
|
+
const tunnelClient = findTunnelClientExecutable();
|
|
123
|
+
const missing = [];
|
|
124
|
+
if (!tunnelClient)
|
|
125
|
+
missing.push("tunnel-client.exe");
|
|
126
|
+
for (const entry of missingLaunchers)
|
|
127
|
+
missing.push(`${entry.mode} launcher`);
|
|
128
|
+
if (missing.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
return {
|
|
131
|
+
status: 409,
|
|
132
|
+
body: {
|
|
133
|
+
ok: false,
|
|
134
|
+
action,
|
|
135
|
+
mode,
|
|
136
|
+
error: "Control Center preflight failed. Start/restart from the Web UI is non-interactive, so required runtime dependencies must be available before launching.",
|
|
137
|
+
missing,
|
|
138
|
+
next_steps: [
|
|
139
|
+
"Install tunnel-client.exe or set TUNNEL_CLIENT_EXE / PATCHWARDEN_TUNNEL_CLIENT_EXE to its full path.",
|
|
140
|
+
`Default checked path: ${DEFAULT_TUNNEL_CLIENT_EXE}`,
|
|
141
|
+
"Verify the launcher files under scripts/launchers are present.",
|
|
142
|
+
"Then retry from PatchWarden Control Center or PatchWarden-Control-Tray.cmd.",
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export async function handleManageAction(res, action, mode) {
|
|
148
|
+
try {
|
|
149
|
+
const preflight = preflightManageAction(action, mode);
|
|
150
|
+
if (preflight) {
|
|
151
|
+
recordEvent("manage." + mode + "." + action + ".preflight_failed", {
|
|
152
|
+
missing: preflight.body.missing,
|
|
153
|
+
});
|
|
154
|
+
sendJson(res, preflight.status, preflight.body);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const result = await runManageAction(action, mode);
|
|
158
|
+
recordEvent("manage." + mode + "." + action, {
|
|
159
|
+
exit_code: result.exitCode,
|
|
160
|
+
ok: result.exitCode === 0,
|
|
161
|
+
});
|
|
162
|
+
sendJson(res, 200, {
|
|
163
|
+
ok: result.exitCode === 0,
|
|
164
|
+
exitCode: result.exitCode,
|
|
165
|
+
stdout: result.stdout,
|
|
166
|
+
stderr: result.stderr,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
recordEvent("manage." + mode + "." + action + ".failed", { error: errorMessage(err) });
|
|
171
|
+
sendJson(res, 500, { error: errorMessage(err) });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function handleOpenLogsFolder(res) {
|
|
175
|
+
try {
|
|
176
|
+
const target = getRuntimeRoot(false);
|
|
177
|
+
let cmd;
|
|
178
|
+
if (process.platform === "win32") {
|
|
179
|
+
cmd = "explorer.exe";
|
|
180
|
+
}
|
|
181
|
+
else if (process.platform === "darwin") {
|
|
182
|
+
cmd = "open";
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
cmd = "xdg-open";
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const child = spawn(cmd, [target], { detached: true, stdio: "ignore" });
|
|
189
|
+
child.on("error", () => { });
|
|
190
|
+
child.unref();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* ignore */
|
|
194
|
+
}
|
|
195
|
+
sendJson(res, 200, { ok: true, path: target });
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
sendJson(res, 500, { error: errorMessage(err) });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ServerResponse } from "node:http";
|
|
2
|
+
export declare function handleDirectSessions(res: ServerResponse): void;
|
|
3
|
+
export declare function handleDirectSessionDetail(res: ServerResponse, sessionId: string): void;
|
|
4
|
+
export declare function handleDirectSessionSafeSummary(res: ServerResponse, sessionId: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Finalize a direct session via safeFinalizeDirectSession. Returns the safe
|
|
7
|
+
* summary on success, or { error } on failure with HTTP 200 (fault tolerance:
|
|
8
|
+
* the UI always gets a JSON body it can render).
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleDirectSessionFinalize(res: ServerResponse, sessionId: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Audit a direct session via safeAuditDirectSession. Returns the safe audit
|
|
13
|
+
* result on success, or { error } on failure (HTTP 200 for fault tolerance).
|
|
14
|
+
*/
|
|
15
|
+
export declare function handleDirectSessionAudit(res: ServerResponse, sessionId: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Hide a direct session from the dashboard list. The session itself is NOT
|
|
18
|
+
* deleted or modified — only the control-center's local
|
|
19
|
+
* hidden-direct-session-ids.json state file is updated. Requires control token
|
|
20
|
+
* (enforced by the POST router).
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleDirectSessionHide(res: ServerResponse, sessionId: string): void;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — Direct sessions (/api/direct-sessions/*).
|
|
3
|
+
*
|
|
4
|
+
* Lists, inspects, finalizes, audits, and hides Direct editing sessions. The
|
|
5
|
+
* list/detail endpoints are read-only and fault-tolerant (missing dir returns
|
|
6
|
+
* an empty list, never 500). The finalize/audit/hide endpoints are POST routes
|
|
7
|
+
* gated by the control token in the server router.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getDirectSessionsDir } from "../../config.js";
|
|
12
|
+
import { safeAuditDirectSession, safeDirectSummary, safeFinalizeDirectSession } from "../../tools/safeViews.js";
|
|
13
|
+
import { fileMtimeIso, isValidDirectSessionId, readHiddenDirectSessionIds, recordEvent, writeHiddenDirectSessionIds, } from "../runtime.js";
|
|
14
|
+
import { config, errorMessage, readJsonFileSafe, readTextFileSafe, sendJson } from "../shared.js";
|
|
15
|
+
function readDirectSessionSummary(sessionDir, sessionId) {
|
|
16
|
+
const sessionFile = join(sessionDir, "session.json");
|
|
17
|
+
if (!existsSync(sessionFile))
|
|
18
|
+
return null;
|
|
19
|
+
const data = readJsonFileSafe(sessionFile);
|
|
20
|
+
if (!data)
|
|
21
|
+
return null;
|
|
22
|
+
// summary.json holds the finalized change summary (changed_files_total, etc.)
|
|
23
|
+
const summaryFile = join(sessionDir, "summary.json");
|
|
24
|
+
const summary = readJsonFileSafe(summaryFile);
|
|
25
|
+
const changedFilesTotal = summary
|
|
26
|
+
? typeof summary.changed_files_total === "number"
|
|
27
|
+
? summary.changed_files_total
|
|
28
|
+
: null
|
|
29
|
+
: null;
|
|
30
|
+
// audit.json (written by audit_session) holds the audit decision
|
|
31
|
+
const auditFile = join(sessionDir, "audit.json");
|
|
32
|
+
const audit = readJsonFileSafe(auditFile);
|
|
33
|
+
const auditDecision = audit
|
|
34
|
+
? typeof audit.decision === "string"
|
|
35
|
+
? audit.decision
|
|
36
|
+
: typeof audit.verdict === "string"
|
|
37
|
+
? audit.verdict
|
|
38
|
+
: null
|
|
39
|
+
: null;
|
|
40
|
+
const auditCheckedAt = audit
|
|
41
|
+
? typeof audit.checked_at === "string"
|
|
42
|
+
? audit.checked_at
|
|
43
|
+
: fileMtimeIso(auditFile)
|
|
44
|
+
: null;
|
|
45
|
+
// verification summary: read from session.json verification_runs (last run)
|
|
46
|
+
let verificationSummary = null;
|
|
47
|
+
if (Array.isArray(data.verification_runs) && data.verification_runs.length > 0) {
|
|
48
|
+
const runs = data.verification_runs;
|
|
49
|
+
verificationSummary = runs[runs.length - 1];
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
session_id: sessionId,
|
|
53
|
+
repo_path: typeof data.repo_path === "string" ? data.repo_path : "",
|
|
54
|
+
resolved_repo_path: typeof data.resolved_repo_path === "string" ? data.resolved_repo_path : "",
|
|
55
|
+
created_at: typeof data.created_at === "string" ? data.created_at : "",
|
|
56
|
+
expires_at: typeof data.expires_at === "string" ? data.expires_at : "",
|
|
57
|
+
finalized: Boolean(data.finalized),
|
|
58
|
+
finalized_at: typeof data.finalized_at === "string" ? data.finalized_at : null,
|
|
59
|
+
audited: Boolean(data.audited),
|
|
60
|
+
changed_files_total: changedFilesTotal,
|
|
61
|
+
verification_summary: verificationSummary,
|
|
62
|
+
audit_decision: auditDecision,
|
|
63
|
+
audit_checked_at: auditCheckedAt,
|
|
64
|
+
title: typeof data.title === "string" ? data.title : "",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function handleDirectSessions(res) {
|
|
68
|
+
try {
|
|
69
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
70
|
+
if (!existsSync(sessionsDir)) {
|
|
71
|
+
// Directory missing -> empty list, never 500.
|
|
72
|
+
sendJson(res, 200, { sessions: [], total: 0, reason: null });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let entries = [];
|
|
76
|
+
try {
|
|
77
|
+
entries = readdirSync(sessionsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
sendJson(res, 200, { sessions: [], total: 0, reason: errorMessage(err) });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Filter out sessions hidden from the dashboard via POST .../hide.
|
|
84
|
+
const hiddenIds = new Set(readHiddenDirectSessionIds());
|
|
85
|
+
const summaries = [];
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (hiddenIds.has(entry.name))
|
|
88
|
+
continue;
|
|
89
|
+
const summary = readDirectSessionSummary(join(sessionsDir, entry.name), entry.name);
|
|
90
|
+
if (summary)
|
|
91
|
+
summaries.push(summary);
|
|
92
|
+
}
|
|
93
|
+
// Sort by created_at descending.
|
|
94
|
+
summaries.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
95
|
+
sendJson(res, 200, { sessions: summaries, total: summaries.length, reason: null });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
sendJson(res, 200, { sessions: [], total: 0, reason: errorMessage(err) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function handleDirectSessionDetail(res, sessionId) {
|
|
102
|
+
try {
|
|
103
|
+
if (sessionId === "." ||
|
|
104
|
+
sessionId === ".." ||
|
|
105
|
+
sessionId.includes("/") ||
|
|
106
|
+
sessionId.includes("\\") ||
|
|
107
|
+
sessionId.includes("\0")) {
|
|
108
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
112
|
+
const sessionDir = join(sessionsDir, sessionId);
|
|
113
|
+
if (!existsSync(sessionDir) || !statSync(sessionDir).isDirectory()) {
|
|
114
|
+
sendJson(res, 404, { error: "Direct session not found" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const summary = readDirectSessionSummary(sessionDir, sessionId);
|
|
118
|
+
sendJson(res, 200, {
|
|
119
|
+
session_id: sessionId,
|
|
120
|
+
summary,
|
|
121
|
+
session: readJsonFileSafe(join(sessionDir, "session.json")),
|
|
122
|
+
summary_md: readTextFileSafe(join(sessionDir, "summary.md")),
|
|
123
|
+
diff_patch: readTextFileSafe(join(sessionDir, "diff.patch")),
|
|
124
|
+
audit_json: readJsonFileSafe(join(sessionDir, "audit.json")),
|
|
125
|
+
audit_md: readTextFileSafe(join(sessionDir, "audit.md")),
|
|
126
|
+
changed_files: readJsonFileSafe(join(sessionDir, "changed-files.json")),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
sendJson(res, 200, { session_id: sessionId, error: errorMessage(err) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function handleDirectSessionSafeSummary(res, sessionId) {
|
|
134
|
+
try {
|
|
135
|
+
if (sessionId === "." ||
|
|
136
|
+
sessionId === ".." ||
|
|
137
|
+
sessionId.includes("/") ||
|
|
138
|
+
sessionId.includes("\\") ||
|
|
139
|
+
sessionId.includes("\0")) {
|
|
140
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sendJson(res, 200, safeDirectSummary(sessionId, { max_items: 12 }));
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
sendJson(res, 200, {
|
|
147
|
+
session_id: sessionId,
|
|
148
|
+
error: errorMessage(err),
|
|
149
|
+
large_logs_omitted: true,
|
|
150
|
+
diff_omitted: true,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Finalize a direct session via safeFinalizeDirectSession. Returns the safe
|
|
156
|
+
* summary on success, or { error } on failure with HTTP 200 (fault tolerance:
|
|
157
|
+
* the UI always gets a JSON body it can render).
|
|
158
|
+
*/
|
|
159
|
+
export async function handleDirectSessionFinalize(res, sessionId) {
|
|
160
|
+
try {
|
|
161
|
+
if (!isValidDirectSessionId(sessionId)) {
|
|
162
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const result = await safeFinalizeDirectSession(sessionId, { max_items: 12 });
|
|
166
|
+
recordEvent("direct_session.finalized", { session_id: sessionId });
|
|
167
|
+
sendJson(res, 200, result);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
sendJson(res, 200, {
|
|
171
|
+
session_id: sessionId,
|
|
172
|
+
error: errorMessage(err),
|
|
173
|
+
large_logs_omitted: true,
|
|
174
|
+
diff_omitted: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Audit a direct session via safeAuditDirectSession. Returns the safe audit
|
|
180
|
+
* result on success, or { error } on failure (HTTP 200 for fault tolerance).
|
|
181
|
+
*/
|
|
182
|
+
export function handleDirectSessionAudit(res, sessionId) {
|
|
183
|
+
try {
|
|
184
|
+
if (!isValidDirectSessionId(sessionId)) {
|
|
185
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const result = safeAuditDirectSession(sessionId, { max_items: 12 });
|
|
189
|
+
recordEvent("direct_session.audited", { session_id: sessionId });
|
|
190
|
+
sendJson(res, 200, result);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
sendJson(res, 200, {
|
|
194
|
+
session_id: sessionId,
|
|
195
|
+
error: errorMessage(err),
|
|
196
|
+
large_logs_omitted: true,
|
|
197
|
+
diff_omitted: true,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Hide a direct session from the dashboard list. The session itself is NOT
|
|
203
|
+
* deleted or modified — only the control-center's local
|
|
204
|
+
* hidden-direct-session-ids.json state file is updated. Requires control token
|
|
205
|
+
* (enforced by the POST router).
|
|
206
|
+
*/
|
|
207
|
+
export function handleDirectSessionHide(res, sessionId) {
|
|
208
|
+
try {
|
|
209
|
+
if (!isValidDirectSessionId(sessionId)) {
|
|
210
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const ids = readHiddenDirectSessionIds();
|
|
214
|
+
if (!ids.includes(sessionId)) {
|
|
215
|
+
ids.push(sessionId);
|
|
216
|
+
writeHiddenDirectSessionIds(ids);
|
|
217
|
+
}
|
|
218
|
+
recordEvent("direct_session.hidden", { session_id: sessionId });
|
|
219
|
+
sendJson(res, 200, { ok: true, hidden: sessionId });
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
sendJson(res, 500, { error: errorMessage(err) });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ServerResponse } from "node:http";
|
|
2
|
+
export declare function handleStatus(res: ServerResponse): Promise<void>;
|
|
3
|
+
export declare function handleControlCenterStatus(res: ServerResponse): void;
|
|
4
|
+
export declare function handleEvents(res: ServerResponse, limit: number): void;
|
|
5
|
+
export declare function handleTunnelUiUrl(res: ServerResponse): void;
|
|
6
|
+
export declare function handleDiagnostics(res: ServerResponse): void;
|