patchwarden 1.1.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +83 -8
- package/README.md +78 -10
- 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 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- 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/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -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 +261 -255
- 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.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- 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/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- 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 +45 -0
- package/dist/tools/evidencePack.js +375 -0
- 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/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- 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/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -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 +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -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 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- 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/policy/projectPolicy.ts +344 -0
- 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 +254 -252
- 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 +251 -0
- 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/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- 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 +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- 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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — task lineage (/api/lineages/*).
|
|
3
|
+
*
|
|
4
|
+
* Lists all lineages under .patchwarden/lineages (bounded to 50, most recently
|
|
5
|
+
* updated first) and serves a single lineage detail. Each record is projected
|
|
6
|
+
* through `toSafeTaskLineage` so full artifact content is bounded.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { type ServerResponse } from "node:http";
|
|
11
|
+
import { toSafeTaskLineage, type SafeTaskLineage, type TaskLineageRecord } from "../../tools/taskLineage.js";
|
|
12
|
+
import { config, errorMessage, readJsonFileSafeUnder, sendJson } from "../shared.js";
|
|
13
|
+
|
|
14
|
+
interface LineageSummary {
|
|
15
|
+
iterations: number;
|
|
16
|
+
main_task_count: number;
|
|
17
|
+
fix_task_count: number;
|
|
18
|
+
cleanup_task_count: number;
|
|
19
|
+
direct_verification: {
|
|
20
|
+
session_id: string;
|
|
21
|
+
status: string;
|
|
22
|
+
audit_decision: string;
|
|
23
|
+
command_count: number;
|
|
24
|
+
passed_commands: number;
|
|
25
|
+
failed_commands: number;
|
|
26
|
+
} | null;
|
|
27
|
+
warnings_count: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function augmentLineageSummary(safe: SafeTaskLineage, record: TaskLineageRecord): SafeTaskLineage & LineageSummary {
|
|
31
|
+
const directSessions = safe.tasks.direct_sessions;
|
|
32
|
+
const firstDirect = directSessions.length > 0 ? directSessions[0] : null;
|
|
33
|
+
const directVerification = firstDirect
|
|
34
|
+
? {
|
|
35
|
+
session_id: firstDirect.session_id,
|
|
36
|
+
status: firstDirect.status || "unknown",
|
|
37
|
+
audit_decision: firstDirect.audit_decision || "not_run",
|
|
38
|
+
command_count: firstDirect.command_count ?? 0,
|
|
39
|
+
passed_commands: firstDirect.passed_commands ?? 0,
|
|
40
|
+
failed_commands: firstDirect.failed_commands ?? 0,
|
|
41
|
+
}
|
|
42
|
+
: null;
|
|
43
|
+
return {
|
|
44
|
+
...safe,
|
|
45
|
+
iterations: record.rounds.length,
|
|
46
|
+
main_task_count: record.main_task ? 1 : 0,
|
|
47
|
+
fix_task_count: record.fix_tasks.length,
|
|
48
|
+
cleanup_task_count: record.cleanup_tasks.length,
|
|
49
|
+
direct_verification: directVerification,
|
|
50
|
+
warnings_count: record.warnings.length,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function handleLineages(res: ServerResponse): void {
|
|
55
|
+
try {
|
|
56
|
+
const root = join(config.workspaceRoot, ".patchwarden", "lineages");
|
|
57
|
+
if (!existsSync(root)) {
|
|
58
|
+
sendJson(res, 200, { lineages: [], total: 0, reason: null });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const lineages = readdirSync(root, { withFileTypes: true })
|
|
62
|
+
.filter((entry) => entry.isDirectory())
|
|
63
|
+
.map((entry) => readJsonFileSafeUnder<TaskLineageRecord>(root, join(entry.name, "lineage.json")))
|
|
64
|
+
.filter((entry): entry is TaskLineageRecord => entry !== null)
|
|
65
|
+
.map((entry) => augmentLineageSummary(toSafeTaskLineage(entry, 6), entry))
|
|
66
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
|
|
67
|
+
.slice(0, 50);
|
|
68
|
+
sendJson(res, 200, { lineages, total: lineages.length, reason: null });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
sendJson(res, 200, { lineages: [], total: 0, reason: errorMessage(err) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function handleLineageDetail(res: ServerResponse, lineageId: string): void {
|
|
75
|
+
try {
|
|
76
|
+
if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
|
|
77
|
+
sendJson(res, 400, { error: "Invalid lineage id" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const data = readJsonFileSafeUnder<TaskLineageRecord>(
|
|
81
|
+
join(config.workspaceRoot, ".patchwarden", "lineages"),
|
|
82
|
+
join(lineageId, "lineage.json")
|
|
83
|
+
);
|
|
84
|
+
if (!data) {
|
|
85
|
+
sendJson(res, 404, { error: "Lineage not found" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
sendJson(res, 200, augmentLineageSummary(toSafeTaskLineage(data, 20), data));
|
|
89
|
+
} catch (err) {
|
|
90
|
+
sendJson(res, 200, { lineage_id: lineageId, error: errorMessage(err) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — project policy + release status.
|
|
3
|
+
*
|
|
4
|
+
* Read-only wrappers around `getProjectPolicySummary` that surface release
|
|
5
|
+
* readiness, blocked commands, and version consistency for the dashboard's
|
|
6
|
+
* release panel. Never performs remote writes (`remote_write_performed` is
|
|
7
|
+
* always false).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { type ServerResponse } from "node:http";
|
|
12
|
+
import { getProjectPolicySummary } from "../../policy/projectPolicy.js";
|
|
13
|
+
import { errorMessage, sendJson } from "../shared.js";
|
|
14
|
+
|
|
15
|
+
export function handleProjectPolicy(res: ServerResponse, repoPath: string): void {
|
|
16
|
+
try {
|
|
17
|
+
sendJson(res, 200, getProjectPolicySummary(repoPath || "."));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
sendJson(res, 200, {
|
|
20
|
+
repo_path: repoPath || ".",
|
|
21
|
+
valid: false,
|
|
22
|
+
issues: [{ code: "policy_unavailable", severity: "error", field: "repo_path", message: errorMessage(err) }],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function handleReleaseStatus(res: ServerResponse, repoPath: string): void {
|
|
28
|
+
try {
|
|
29
|
+
const policy = getProjectPolicySummary(repoPath || ".");
|
|
30
|
+
const readiness = policy.release_readiness;
|
|
31
|
+
const requiredCommands = readiness.required_commands.map((c) => ({
|
|
32
|
+
command: c.command,
|
|
33
|
+
allowed: c.allowed,
|
|
34
|
+
blocked_reason: c.allowed ? null : (c.reason || "not_allowed"),
|
|
35
|
+
}));
|
|
36
|
+
const commandsBlockedCount = requiredCommands.filter((c) => !c.allowed).length;
|
|
37
|
+
const hasPackageJson = existsSync(join(policy.resolved_repo_path, "package.json"));
|
|
38
|
+
const versionSource = hasPackageJson
|
|
39
|
+
? "package.json"
|
|
40
|
+
: (repoPath || ".") === "."
|
|
41
|
+
? "workspace_root"
|
|
42
|
+
: "unknown";
|
|
43
|
+
|
|
44
|
+
let readyState: string;
|
|
45
|
+
if (!policy.valid || commandsBlockedCount > 0) {
|
|
46
|
+
readyState = "blocked";
|
|
47
|
+
} else if (readiness.version === null) {
|
|
48
|
+
readyState = "unknown";
|
|
49
|
+
} else if (readiness.version_consistent === true) {
|
|
50
|
+
readyState = "ready";
|
|
51
|
+
} else {
|
|
52
|
+
readyState = "blocked";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
sendJson(res, 200, {
|
|
56
|
+
repo_path: repoPath || ".",
|
|
57
|
+
resolved_repo_path: policy.resolved_repo_path,
|
|
58
|
+
policy_valid: policy.valid,
|
|
59
|
+
policy_issue_count: policy.issues.length,
|
|
60
|
+
policy_issues: policy.issues.slice(0, 10),
|
|
61
|
+
release_readiness: readiness,
|
|
62
|
+
package_name: readiness.package_name,
|
|
63
|
+
version_source: versionSource,
|
|
64
|
+
version_consistent: readiness.version_consistent,
|
|
65
|
+
required_commands: requiredCommands,
|
|
66
|
+
commands_blocked_count: commandsBlockedCount,
|
|
67
|
+
ready_state: readyState,
|
|
68
|
+
next_action: policy.valid && readiness.version_consistent !== false
|
|
69
|
+
? "Run release_check via create_task template, or run_task_loop with template=release_check."
|
|
70
|
+
: "Fix project-policy or version consistency issues before release preparation.",
|
|
71
|
+
remote_write_performed: false,
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
sendJson(res, 200, {
|
|
75
|
+
repo_path: repoPath || ".",
|
|
76
|
+
policy_valid: false,
|
|
77
|
+
error: errorMessage(err),
|
|
78
|
+
remote_write_performed: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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 { type ServerResponse } from "node:http";
|
|
16
|
+
import { recordEvent } from "../runtime.js";
|
|
17
|
+
import { DEFAULT_TUNNEL_CLIENT_EXE, errorMessage, getRuntimeRoot, manageScriptPath, projectRoot, sendJson } from "../shared.js";
|
|
18
|
+
|
|
19
|
+
type ControlMode = "core" | "direct";
|
|
20
|
+
|
|
21
|
+
interface ManageResult {
|
|
22
|
+
exitCode: number;
|
|
23
|
+
stdout: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runManageAction(action: string, mode: string): Promise<ManageResult> {
|
|
28
|
+
return new Promise((resolveP, rejectP) => {
|
|
29
|
+
let child;
|
|
30
|
+
try {
|
|
31
|
+
child = spawn(
|
|
32
|
+
"powershell.exe",
|
|
33
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", manageScriptPath, action, mode, "-Background"],
|
|
34
|
+
{ cwd: projectRoot, windowsHide: true }
|
|
35
|
+
);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
rejectP(err);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let stdout = "";
|
|
41
|
+
let stderr = "";
|
|
42
|
+
let settled = false;
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
try { child.kill(); } catch { /* ignore */ }
|
|
47
|
+
rejectP(new Error(`manage-patchwarden.ps1 timed out after 60s (action=${action}, mode=${mode})`));
|
|
48
|
+
}, 60_000);
|
|
49
|
+
child.stdout.on("data", (d: Buffer) => {
|
|
50
|
+
stdout += d.toString("utf-8");
|
|
51
|
+
});
|
|
52
|
+
child.stderr.on("data", (d: Buffer) => {
|
|
53
|
+
stderr += d.toString("utf-8");
|
|
54
|
+
});
|
|
55
|
+
child.on("error", (err) => {
|
|
56
|
+
if (settled) return;
|
|
57
|
+
settled = true;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
rejectP(err);
|
|
60
|
+
});
|
|
61
|
+
child.on("close", (code) => {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
settled = true;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
resolveP({ exitCode: code ?? -1, stdout, stderr });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function selectedControlModes(mode: string): ControlMode[] {
|
|
71
|
+
if (mode === "core" || mode === "direct") return [mode];
|
|
72
|
+
return ["core", "direct"];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function launcherPathForMode(mode: ControlMode): string {
|
|
76
|
+
const launcherName = mode === "direct" ? "Start-PatchWarden-Direct-Tunnel.cmd" : "Start-PatchWarden-Tunnel.cmd";
|
|
77
|
+
return join(projectRoot, "scripts", "launchers", launcherName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findExecutableOnPath(fileName: string): string | null {
|
|
81
|
+
const pathValue = process.env.PATH || "";
|
|
82
|
+
const extensions =
|
|
83
|
+
process.platform === "win32"
|
|
84
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
|
|
85
|
+
: [""];
|
|
86
|
+
for (const entry of pathValue.split(delimiter)) {
|
|
87
|
+
if (!entry) continue;
|
|
88
|
+
const direct = join(entry, fileName);
|
|
89
|
+
if (existsSync(direct)) return direct;
|
|
90
|
+
if (process.platform === "win32" && !extname(fileName)) {
|
|
91
|
+
for (const ext of extensions) {
|
|
92
|
+
const candidate = join(entry, fileName + ext.toLowerCase());
|
|
93
|
+
if (existsSync(candidate)) return candidate;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findTunnelClientExecutable(): string | null {
|
|
101
|
+
if (process.env.PATCHWARDEN_CONTROL_FORCE_MISSING_TUNNEL_CLIENT === "1") return null;
|
|
102
|
+
const explicit = process.env.TUNNEL_CLIENT_EXE || process.env.PATCHWARDEN_TUNNEL_CLIENT_EXE;
|
|
103
|
+
if (explicit && existsSync(explicit)) return explicit;
|
|
104
|
+
const fromPath = findExecutableOnPath("tunnel-client.exe") ?? findExecutableOnPath("tunnel-client");
|
|
105
|
+
if (fromPath) return fromPath;
|
|
106
|
+
|
|
107
|
+
const candidates = [
|
|
108
|
+
DEFAULT_TUNNEL_CLIENT_EXE,
|
|
109
|
+
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "patchwarden", "tunnel-client.exe") : null,
|
|
110
|
+
process.env.APPDATA ? join(process.env.APPDATA, "tunnel-client", "tunnel-client.exe") : null,
|
|
111
|
+
join(homedir(), "tunnel-client", "tunnel-client.exe"),
|
|
112
|
+
].filter((v): v is string => typeof v === "string" && v.length > 0);
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
if (existsSync(candidate)) return candidate;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Re-exported so the status route can surface tunnel-client availability in
|
|
120
|
+
// /api/status without duplicating the discovery logic.
|
|
121
|
+
export { findTunnelClientExecutable };
|
|
122
|
+
|
|
123
|
+
function preflightManageAction(action: string, mode: string): { status: number; body: Record<string, unknown> } | null {
|
|
124
|
+
if (action !== "start" && action !== "restart") return null;
|
|
125
|
+
|
|
126
|
+
const missingLaunchers = selectedControlModes(mode)
|
|
127
|
+
.map((m) => ({ mode: m, path: launcherPathForMode(m) }))
|
|
128
|
+
.filter((entry) => !existsSync(entry.path));
|
|
129
|
+
const tunnelClient = findTunnelClientExecutable();
|
|
130
|
+
const missing: string[] = [];
|
|
131
|
+
if (!tunnelClient) missing.push("tunnel-client.exe");
|
|
132
|
+
for (const entry of missingLaunchers) missing.push(`${entry.mode} launcher`);
|
|
133
|
+
|
|
134
|
+
if (missing.length === 0) return null;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
status: 409,
|
|
138
|
+
body: {
|
|
139
|
+
ok: false,
|
|
140
|
+
action,
|
|
141
|
+
mode,
|
|
142
|
+
error:
|
|
143
|
+
"Control Center preflight failed. Start/restart from the Web UI is non-interactive, so required runtime dependencies must be available before launching.",
|
|
144
|
+
missing,
|
|
145
|
+
next_steps: [
|
|
146
|
+
"Install tunnel-client.exe or set TUNNEL_CLIENT_EXE / PATCHWARDEN_TUNNEL_CLIENT_EXE to its full path.",
|
|
147
|
+
`Default checked path: ${DEFAULT_TUNNEL_CLIENT_EXE}`,
|
|
148
|
+
"Verify the launcher files under scripts/launchers are present.",
|
|
149
|
+
"Then retry from PatchWarden Control Center or PatchWarden-Control-Tray.cmd.",
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function handleManageAction(res: ServerResponse, action: string, mode: string): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
const preflight = preflightManageAction(action, mode);
|
|
158
|
+
if (preflight) {
|
|
159
|
+
recordEvent("manage." + mode + "." + action + ".preflight_failed", {
|
|
160
|
+
missing: preflight.body.missing,
|
|
161
|
+
});
|
|
162
|
+
sendJson(res, preflight.status, preflight.body);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const result = await runManageAction(action, mode);
|
|
166
|
+
recordEvent("manage." + mode + "." + action, {
|
|
167
|
+
exit_code: result.exitCode,
|
|
168
|
+
ok: result.exitCode === 0,
|
|
169
|
+
});
|
|
170
|
+
sendJson(res, 200, {
|
|
171
|
+
ok: result.exitCode === 0,
|
|
172
|
+
exitCode: result.exitCode,
|
|
173
|
+
stdout: result.stdout,
|
|
174
|
+
stderr: result.stderr,
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
recordEvent("manage." + mode + "." + action + ".failed", { error: errorMessage(err) });
|
|
178
|
+
sendJson(res, 500, { error: errorMessage(err) });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function handleOpenLogsFolder(res: ServerResponse): void {
|
|
183
|
+
try {
|
|
184
|
+
const target = getRuntimeRoot(false);
|
|
185
|
+
let cmd: string;
|
|
186
|
+
if (process.platform === "win32") {
|
|
187
|
+
cmd = "explorer.exe";
|
|
188
|
+
} else if (process.platform === "darwin") {
|
|
189
|
+
cmd = "open";
|
|
190
|
+
} else {
|
|
191
|
+
cmd = "xdg-open";
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const child = spawn(cmd, [target], { detached: true, stdio: "ignore" });
|
|
195
|
+
child.on("error", () => { /* ignore spawn errors */ });
|
|
196
|
+
child.unref();
|
|
197
|
+
} catch {
|
|
198
|
+
/* ignore */
|
|
199
|
+
}
|
|
200
|
+
sendJson(res, 200, { ok: true, path: target });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
sendJson(res, 500, { error: errorMessage(err) });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
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 { type ServerResponse } from "node:http";
|
|
12
|
+
import { getDirectSessionsDir } from "../../config.js";
|
|
13
|
+
import { safeAuditDirectSession, safeDirectSummary, safeFinalizeDirectSession } from "../../tools/safeViews.js";
|
|
14
|
+
import {
|
|
15
|
+
fileMtimeIso,
|
|
16
|
+
isValidDirectSessionId,
|
|
17
|
+
readHiddenDirectSessionIds,
|
|
18
|
+
recordEvent,
|
|
19
|
+
writeHiddenDirectSessionIds,
|
|
20
|
+
} from "../runtime.js";
|
|
21
|
+
import { config, errorMessage, readJsonFileSafe, readTextFileSafe, sendJson } from "../shared.js";
|
|
22
|
+
|
|
23
|
+
interface DirectSessionSummary {
|
|
24
|
+
session_id: string;
|
|
25
|
+
repo_path: string;
|
|
26
|
+
resolved_repo_path: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
expires_at: string;
|
|
29
|
+
finalized: boolean;
|
|
30
|
+
finalized_at: string | null;
|
|
31
|
+
audited: boolean;
|
|
32
|
+
changed_files_total: number | null;
|
|
33
|
+
verification_summary: unknown | null;
|
|
34
|
+
audit_decision: string | null;
|
|
35
|
+
audit_checked_at: string | null;
|
|
36
|
+
title: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readDirectSessionSummary(sessionDir: string, sessionId: string): DirectSessionSummary | null {
|
|
40
|
+
const sessionFile = join(sessionDir, "session.json");
|
|
41
|
+
if (!existsSync(sessionFile)) return null;
|
|
42
|
+
const data = readJsonFileSafe<Record<string, unknown>>(sessionFile);
|
|
43
|
+
if (!data) return null;
|
|
44
|
+
|
|
45
|
+
// summary.json holds the finalized change summary (changed_files_total, etc.)
|
|
46
|
+
const summaryFile = join(sessionDir, "summary.json");
|
|
47
|
+
const summary = readJsonFileSafe<Record<string, unknown>>(summaryFile);
|
|
48
|
+
const changedFilesTotal = summary
|
|
49
|
+
? typeof summary.changed_files_total === "number"
|
|
50
|
+
? summary.changed_files_total
|
|
51
|
+
: null
|
|
52
|
+
: null;
|
|
53
|
+
|
|
54
|
+
// audit.json (written by audit_session) holds the audit decision
|
|
55
|
+
const auditFile = join(sessionDir, "audit.json");
|
|
56
|
+
const audit = readJsonFileSafe<Record<string, unknown>>(auditFile);
|
|
57
|
+
const auditDecision = audit
|
|
58
|
+
? typeof audit.decision === "string"
|
|
59
|
+
? audit.decision
|
|
60
|
+
: typeof audit.verdict === "string"
|
|
61
|
+
? audit.verdict
|
|
62
|
+
: null
|
|
63
|
+
: null;
|
|
64
|
+
const auditCheckedAt = audit
|
|
65
|
+
? typeof audit.checked_at === "string"
|
|
66
|
+
? audit.checked_at
|
|
67
|
+
: fileMtimeIso(auditFile)
|
|
68
|
+
: null;
|
|
69
|
+
|
|
70
|
+
// verification summary: read from session.json verification_runs (last run)
|
|
71
|
+
let verificationSummary: unknown | null = null;
|
|
72
|
+
if (Array.isArray(data.verification_runs) && data.verification_runs.length > 0) {
|
|
73
|
+
const runs = data.verification_runs as Array<Record<string, unknown>>;
|
|
74
|
+
verificationSummary = runs[runs.length - 1];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
session_id: sessionId,
|
|
79
|
+
repo_path: typeof data.repo_path === "string" ? data.repo_path : "",
|
|
80
|
+
resolved_repo_path: typeof data.resolved_repo_path === "string" ? data.resolved_repo_path : "",
|
|
81
|
+
created_at: typeof data.created_at === "string" ? data.created_at : "",
|
|
82
|
+
expires_at: typeof data.expires_at === "string" ? data.expires_at : "",
|
|
83
|
+
finalized: Boolean(data.finalized),
|
|
84
|
+
finalized_at: typeof data.finalized_at === "string" ? data.finalized_at : null,
|
|
85
|
+
audited: Boolean(data.audited),
|
|
86
|
+
changed_files_total: changedFilesTotal,
|
|
87
|
+
verification_summary: verificationSummary,
|
|
88
|
+
audit_decision: auditDecision,
|
|
89
|
+
audit_checked_at: auditCheckedAt,
|
|
90
|
+
title: typeof data.title === "string" ? data.title : "",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function handleDirectSessions(res: ServerResponse): void {
|
|
95
|
+
try {
|
|
96
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
97
|
+
if (!existsSync(sessionsDir)) {
|
|
98
|
+
// Directory missing -> empty list, never 500.
|
|
99
|
+
sendJson(res, 200, { sessions: [], total: 0, reason: null });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let entries: import("node:fs").Dirent[] = [];
|
|
103
|
+
try {
|
|
104
|
+
entries = readdirSync(sessionsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
105
|
+
} catch (err) {
|
|
106
|
+
sendJson(res, 200, { sessions: [], total: 0, reason: errorMessage(err) });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Filter out sessions hidden from the dashboard via POST .../hide.
|
|
110
|
+
const hiddenIds = new Set(readHiddenDirectSessionIds());
|
|
111
|
+
const summaries: DirectSessionSummary[] = [];
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (hiddenIds.has(entry.name)) continue;
|
|
114
|
+
const summary = readDirectSessionSummary(join(sessionsDir, entry.name), entry.name);
|
|
115
|
+
if (summary) summaries.push(summary);
|
|
116
|
+
}
|
|
117
|
+
// Sort by created_at descending.
|
|
118
|
+
summaries.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
119
|
+
sendJson(res, 200, { sessions: summaries, total: summaries.length, reason: null });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
sendJson(res, 200, { sessions: [], total: 0, reason: errorMessage(err) });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function handleDirectSessionDetail(res: ServerResponse, sessionId: string): void {
|
|
126
|
+
try {
|
|
127
|
+
if (
|
|
128
|
+
sessionId === "." ||
|
|
129
|
+
sessionId === ".." ||
|
|
130
|
+
sessionId.includes("/") ||
|
|
131
|
+
sessionId.includes("\\") ||
|
|
132
|
+
sessionId.includes("\0")
|
|
133
|
+
) {
|
|
134
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
138
|
+
const sessionDir = join(sessionsDir, sessionId);
|
|
139
|
+
if (!existsSync(sessionDir) || !statSync(sessionDir).isDirectory()) {
|
|
140
|
+
sendJson(res, 404, { error: "Direct session not found" });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const summary = readDirectSessionSummary(sessionDir, sessionId);
|
|
144
|
+
sendJson(res, 200, {
|
|
145
|
+
session_id: sessionId,
|
|
146
|
+
summary,
|
|
147
|
+
session: readJsonFileSafe(join(sessionDir, "session.json")),
|
|
148
|
+
summary_md: readTextFileSafe(join(sessionDir, "summary.md")),
|
|
149
|
+
diff_patch: readTextFileSafe(join(sessionDir, "diff.patch")),
|
|
150
|
+
audit_json: readJsonFileSafe(join(sessionDir, "audit.json")),
|
|
151
|
+
audit_md: readTextFileSafe(join(sessionDir, "audit.md")),
|
|
152
|
+
changed_files: readJsonFileSafe(join(sessionDir, "changed-files.json")),
|
|
153
|
+
});
|
|
154
|
+
} catch (err) {
|
|
155
|
+
sendJson(res, 200, { session_id: sessionId, error: errorMessage(err) });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function handleDirectSessionSafeSummary(res: ServerResponse, sessionId: string): void {
|
|
160
|
+
try {
|
|
161
|
+
if (
|
|
162
|
+
sessionId === "." ||
|
|
163
|
+
sessionId === ".." ||
|
|
164
|
+
sessionId.includes("/") ||
|
|
165
|
+
sessionId.includes("\\") ||
|
|
166
|
+
sessionId.includes("\0")
|
|
167
|
+
) {
|
|
168
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
sendJson(res, 200, safeDirectSummary(sessionId, { max_items: 12 }));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
sendJson(res, 200, {
|
|
174
|
+
session_id: sessionId,
|
|
175
|
+
error: errorMessage(err),
|
|
176
|
+
large_logs_omitted: true,
|
|
177
|
+
diff_omitted: true,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Finalize a direct session via safeFinalizeDirectSession. Returns the safe
|
|
184
|
+
* summary on success, or { error } on failure with HTTP 200 (fault tolerance:
|
|
185
|
+
* the UI always gets a JSON body it can render).
|
|
186
|
+
*/
|
|
187
|
+
export async function handleDirectSessionFinalize(res: ServerResponse, sessionId: string): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
if (!isValidDirectSessionId(sessionId)) {
|
|
190
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const result = await safeFinalizeDirectSession(sessionId, { max_items: 12 });
|
|
194
|
+
recordEvent("direct_session.finalized", { session_id: sessionId });
|
|
195
|
+
sendJson(res, 200, result);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
sendJson(res, 200, {
|
|
198
|
+
session_id: sessionId,
|
|
199
|
+
error: errorMessage(err),
|
|
200
|
+
large_logs_omitted: true,
|
|
201
|
+
diff_omitted: true,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Audit a direct session via safeAuditDirectSession. Returns the safe audit
|
|
208
|
+
* result on success, or { error } on failure (HTTP 200 for fault tolerance).
|
|
209
|
+
*/
|
|
210
|
+
export function handleDirectSessionAudit(res: ServerResponse, sessionId: string): void {
|
|
211
|
+
try {
|
|
212
|
+
if (!isValidDirectSessionId(sessionId)) {
|
|
213
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const result = safeAuditDirectSession(sessionId, { max_items: 12 });
|
|
217
|
+
recordEvent("direct_session.audited", { session_id: sessionId });
|
|
218
|
+
sendJson(res, 200, result);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
sendJson(res, 200, {
|
|
221
|
+
session_id: sessionId,
|
|
222
|
+
error: errorMessage(err),
|
|
223
|
+
large_logs_omitted: true,
|
|
224
|
+
diff_omitted: true,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Hide a direct session from the dashboard list. The session itself is NOT
|
|
231
|
+
* deleted or modified — only the control-center's local
|
|
232
|
+
* hidden-direct-session-ids.json state file is updated. Requires control token
|
|
233
|
+
* (enforced by the POST router).
|
|
234
|
+
*/
|
|
235
|
+
export function handleDirectSessionHide(res: ServerResponse, sessionId: string): void {
|
|
236
|
+
try {
|
|
237
|
+
if (!isValidDirectSessionId(sessionId)) {
|
|
238
|
+
sendJson(res, 400, { error: "Invalid session id" });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const ids = readHiddenDirectSessionIds();
|
|
242
|
+
if (!ids.includes(sessionId)) {
|
|
243
|
+
ids.push(sessionId);
|
|
244
|
+
writeHiddenDirectSessionIds(ids);
|
|
245
|
+
}
|
|
246
|
+
recordEvent("direct_session.hidden", { session_id: sessionId });
|
|
247
|
+
sendJson(res, 200, { ok: true, hidden: sessionId });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
sendJson(res, 500, { error: errorMessage(err) });
|
|
250
|
+
}
|
|
251
|
+
}
|