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
package/dist/doctor.js
CHANGED
|
@@ -22,31 +22,8 @@ import { buildToolRegistry } from "./tools/toolRegistry.js";
|
|
|
22
22
|
import { runAllSchemaDriftChecks } from "./tools/schemaDriftCheck.js";
|
|
23
23
|
import { runReleaseGateCheck } from "./release/releaseGate.js";
|
|
24
24
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let warn = 0;
|
|
28
|
-
let fail = 0;
|
|
29
|
-
const results = [];
|
|
30
|
-
function check(name, condition, detail) {
|
|
31
|
-
if (condition) {
|
|
32
|
-
results.push(`[OK] ${name}`);
|
|
33
|
-
ok++;
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
results.push(`[FAIL] ${name}${detail ? " — " + detail : ""}`);
|
|
37
|
-
fail++;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function warnCheck(name, condition, detail) {
|
|
41
|
-
if (condition) {
|
|
42
|
-
results.push(`[OK] ${name}`);
|
|
43
|
-
ok++;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
results.push(`[WARN] ${name}${detail ? " — " + detail : ""}`);
|
|
47
|
-
warn++;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
25
|
+
import { logger } from "./logging.js";
|
|
26
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
50
27
|
function cmd(cmdStr) {
|
|
51
28
|
try {
|
|
52
29
|
return execSync(cmdStr, {
|
|
@@ -59,81 +36,124 @@ function cmd(cmdStr) {
|
|
|
59
36
|
return "";
|
|
60
37
|
}
|
|
61
38
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
39
|
+
function okResult(message, details) {
|
|
40
|
+
return { level: "OK", message, details };
|
|
41
|
+
}
|
|
42
|
+
function warnResult(message, details) {
|
|
43
|
+
return { level: "WARN", message, details };
|
|
44
|
+
}
|
|
45
|
+
function failResult(message, details) {
|
|
46
|
+
return { level: "FAIL", message, details };
|
|
47
|
+
}
|
|
48
|
+
/** Mirrors original check(): OK if condition true, FAIL otherwise. */
|
|
49
|
+
function checkResult(name, condition, detail) {
|
|
50
|
+
return condition ? okResult(name, detail) : failResult(name, detail);
|
|
51
|
+
}
|
|
52
|
+
/** Mirrors original warnCheck(): OK if condition true, WARN otherwise. */
|
|
53
|
+
function warnCheckResult(name, condition, detail) {
|
|
54
|
+
return condition ? okResult(name, detail) : warnResult(name, detail);
|
|
55
|
+
}
|
|
56
|
+
function formatResult(result) {
|
|
57
|
+
const tag = result.level === "OK" ? "[OK] " : result.level === "WARN" ? "[WARN] " : "[FAIL] ";
|
|
58
|
+
if (result.level !== "OK" && result.details) {
|
|
59
|
+
return `${tag}${result.message} — ${result.details}`;
|
|
82
60
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
61
|
+
return `${tag}${result.message}`;
|
|
62
|
+
}
|
|
63
|
+
// ── Checks ─────────────────────────────────────────────────────────
|
|
64
|
+
const checkNodeVersion = {
|
|
65
|
+
id: "node-version",
|
|
66
|
+
description: "Node.js version",
|
|
67
|
+
run() {
|
|
68
|
+
const nodeVer = process.version;
|
|
69
|
+
const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
|
|
70
|
+
const detail = nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`;
|
|
71
|
+
return [checkResult("Node.js version", nodeMajor >= 18, detail)];
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const checkNpmAvailable = {
|
|
75
|
+
id: "npm-available",
|
|
76
|
+
description: "npm available",
|
|
77
|
+
run() {
|
|
78
|
+
const npmVer = cmd("npm --version");
|
|
79
|
+
return [checkResult("npm available", npmVer !== "", npmVer || "npm not found in PATH")];
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
// Local-only medium-risk confirmation entrypoint. It must exist as a package
|
|
83
|
+
// binary but must not be exposed as an MCP tool.
|
|
84
|
+
const checkConfirmPackage = {
|
|
85
|
+
id: "confirm-package",
|
|
86
|
+
description: "patchwarden-confirm package",
|
|
87
|
+
run() {
|
|
88
|
+
try {
|
|
89
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
90
|
+
return [
|
|
91
|
+
checkResult("patchwarden-confirm package binary", packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js", packageJson.bin?.["patchwarden-confirm"] || "missing"),
|
|
92
|
+
checkResult("patchwarden-confirm compiled entrypoint", existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")), "run npm.cmd run build"),
|
|
93
|
+
];
|
|
96
94
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
95
|
+
catch (error) {
|
|
96
|
+
return [checkResult("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error))];
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const checkGitAvailable = {
|
|
101
|
+
id: "git-available",
|
|
102
|
+
description: "Git available",
|
|
103
|
+
run() {
|
|
104
|
+
const gitVer = cmd("git --version");
|
|
105
|
+
return [warnCheckResult("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work")];
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const checkConfigFile = {
|
|
109
|
+
id: "config-file",
|
|
110
|
+
description: "Config file exists",
|
|
111
|
+
run(context) {
|
|
112
|
+
const configDetail = context.configPathUsed
|
|
113
|
+
? context.configPathUsed
|
|
114
|
+
: 'Create one: cp examples/config.example.json patchwarden.config.json';
|
|
115
|
+
if (context.allowDefaultConfig) {
|
|
116
|
+
return [warnCheckResult("Config file exists", context.configPathUsed !== "", configDetail)];
|
|
117
|
+
}
|
|
118
|
+
return [checkResult("Config file exists", context.configPathUsed !== "", configDetail)];
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const checkPatchwardenConfigEnv = {
|
|
122
|
+
id: "patchwarden-config-env",
|
|
123
|
+
description: "PATCHWARDEN_CONFIG env",
|
|
124
|
+
run() {
|
|
125
|
+
if (process.env.PATCHWARDEN_CONFIG) {
|
|
126
|
+
return [okResult(`PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`)];
|
|
127
|
+
}
|
|
128
|
+
return [okResult("PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)")];
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const checkConfigParseable = {
|
|
132
|
+
id: "config-parseable",
|
|
133
|
+
description: "Config parseable",
|
|
134
|
+
run(context) {
|
|
135
|
+
if (context.config) {
|
|
136
|
+
return [checkResult("Config parseable", true, `workspaceRoot: ${context.config.workspaceRoot}`)];
|
|
137
|
+
}
|
|
138
|
+
return [checkResult("Config parseable", false, context.configError || "unknown error")];
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const checkWorkspaceRoot = {
|
|
142
|
+
id: "workspace-root",
|
|
143
|
+
description: "workspaceRoot checks",
|
|
144
|
+
run(context) {
|
|
145
|
+
if (!context.config)
|
|
146
|
+
return [];
|
|
147
|
+
const results = [];
|
|
148
|
+
const ws = normalize(resolve(context.config.workspaceRoot));
|
|
128
149
|
const exists = existsSync(ws);
|
|
129
|
-
|
|
150
|
+
results.push(checkResult("workspaceRoot exists", exists, ws));
|
|
130
151
|
let isDir = false;
|
|
131
152
|
try {
|
|
132
153
|
isDir = statSync(ws).isDirectory();
|
|
133
154
|
}
|
|
134
155
|
catch { }
|
|
135
|
-
|
|
136
|
-
// Danger checks
|
|
156
|
+
results.push(checkResult("workspaceRoot is directory", isDir, ws));
|
|
137
157
|
const dangerousRoots = [
|
|
138
158
|
{ pattern: /^[A-Za-z]:\\?$/, label: "drive root" },
|
|
139
159
|
{ pattern: /\\Users\\[^\\]+$/, label: "user home directory" },
|
|
@@ -142,258 +162,365 @@ async function main() {
|
|
|
142
162
|
{ pattern: /\\Documents$/, label: "Documents" },
|
|
143
163
|
];
|
|
144
164
|
for (const { pattern, label } of dangerousRoots) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
results.push(`[WARN] workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`);
|
|
148
|
-
warn++;
|
|
165
|
+
if (pattern.test(ws)) {
|
|
166
|
+
results.push(warnResult(`workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`));
|
|
149
167
|
}
|
|
150
168
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
return results;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const checkPathGuard = {
|
|
173
|
+
id: "path-guard",
|
|
174
|
+
description: "Path guard test",
|
|
175
|
+
run(context) {
|
|
176
|
+
if (!context.config)
|
|
177
|
+
return [];
|
|
178
|
+
const results = [];
|
|
154
179
|
try {
|
|
155
|
-
guardPath("test-file.txt", config.workspaceRoot);
|
|
156
|
-
results.push(
|
|
157
|
-
ok++;
|
|
180
|
+
guardPath("test-file.txt", context.config.workspaceRoot);
|
|
181
|
+
results.push(okResult("pathGuard allows workspace-internal path"));
|
|
158
182
|
}
|
|
159
183
|
catch (err) {
|
|
160
|
-
results.push(`
|
|
161
|
-
fail++;
|
|
184
|
+
results.push(failResult(`pathGuard rejects internal path: ${err instanceof Error ? err.message : String(err)}`));
|
|
162
185
|
}
|
|
163
186
|
try {
|
|
164
|
-
guardPath("../outside", config.workspaceRoot);
|
|
165
|
-
results.push(
|
|
166
|
-
fail++;
|
|
187
|
+
guardPath("../outside", context.config.workspaceRoot);
|
|
188
|
+
results.push(failResult("pathGuard should have blocked ../escape"));
|
|
167
189
|
}
|
|
168
190
|
catch {
|
|
169
|
-
results.push(
|
|
170
|
-
ok++;
|
|
191
|
+
results.push(okResult("pathGuard blocks ../ path escape"));
|
|
171
192
|
}
|
|
172
193
|
try {
|
|
173
|
-
const relativeRepo = guardWorkspacePath(".", config.workspaceRoot);
|
|
174
|
-
const absoluteRepo = guardWorkspacePath(config.workspaceRoot, config.workspaceRoot);
|
|
175
|
-
|
|
194
|
+
const relativeRepo = guardWorkspacePath(".", context.config.workspaceRoot);
|
|
195
|
+
const absoluteRepo = guardWorkspacePath(context.config.workspaceRoot, context.config.workspaceRoot);
|
|
196
|
+
results.push(checkResult("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo));
|
|
176
197
|
}
|
|
177
198
|
catch (error) {
|
|
178
|
-
|
|
199
|
+
results.push(checkResult("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error)));
|
|
179
200
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
201
|
+
return results;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const checkSensitiveGuard = {
|
|
205
|
+
id: "sensitive-guard",
|
|
206
|
+
description: "Sensitive file guard test",
|
|
207
|
+
run() {
|
|
208
|
+
const results = [];
|
|
209
|
+
const sensitivePaths = [".env", ".ssh/id_rsa", "token.json", "credentials"];
|
|
210
|
+
for (const sp of sensitivePaths) {
|
|
211
|
+
const blocked = isSensitivePath(sp);
|
|
212
|
+
if (blocked) {
|
|
213
|
+
results.push(okResult(`sensitiveGuard blocks "${sp}"`));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
results.push(failResult(`sensitiveGuard does NOT block "${sp}"`));
|
|
217
|
+
}
|
|
188
218
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
219
|
+
return results;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const checkSavePlanRules = {
|
|
223
|
+
id: "save-plan-rules",
|
|
224
|
+
description: "save_plan guard test",
|
|
225
|
+
run() {
|
|
226
|
+
const results = [];
|
|
227
|
+
try {
|
|
228
|
+
guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
|
|
229
|
+
results.push(okResult("save_plan allows normal development plans"));
|
|
192
230
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
|
|
196
|
-
results.push("[OK] save_plan allows normal development plans");
|
|
197
|
-
ok++;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
results.push("[FAIL] save_plan incorrectly blocks a normal development plan");
|
|
201
|
-
fail++;
|
|
202
|
-
}
|
|
203
|
-
try {
|
|
204
|
-
guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
|
|
205
|
-
results.push("[FAIL] save_plan security rule did not block credential access");
|
|
206
|
-
fail++;
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
results.push("[OK] save_plan security rules loaded");
|
|
210
|
-
ok++;
|
|
211
|
-
}
|
|
212
|
-
const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
|
|
213
|
-
check("Read-only task artifact allowlist", requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)), requiredReadOnlyFiles.join(", "));
|
|
214
|
-
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
215
|
-
check("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION, `${PATCHWARDEN_VERSION} vs ${packageJson.version}`);
|
|
216
|
-
check("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")), "scripts/checks/mcp-manifest-check.js");
|
|
217
|
-
const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
218
|
-
try {
|
|
219
|
-
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
220
|
-
const fullTools = getToolDefs();
|
|
221
|
-
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
|
|
222
|
-
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
|
|
223
|
-
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
|
|
224
|
-
check("Full tool profile exposes 54 tools", fullTools.length === 54, `${fullTools.length} tools`);
|
|
225
|
-
check(`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`, JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES), coreTools.map((tool) => tool.name).join(", "));
|
|
226
|
-
check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
|
|
227
|
-
createSchema?.properties?.verify_commands &&
|
|
228
|
-
waitSchema?.properties?.timeout_seconds &&
|
|
229
|
-
waitSchema?.properties?.wait_seconds));
|
|
230
|
-
// chatgpt_direct profile checks
|
|
231
|
-
const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
|
|
232
|
-
check("chatgpt_direct disabled exposes only health_check (degraded mode)", directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check", `${directDisabledTools.map((t) => t.name).join(", ")}`);
|
|
233
|
-
if (config?.enableDirectProfile) {
|
|
234
|
-
const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
|
|
235
|
-
check(`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`, JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES), directEnabledTools.map((tool) => tool.name).join(", "));
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
|
|
239
|
-
ok++;
|
|
231
|
+
catch {
|
|
232
|
+
results.push(failResult("save_plan incorrectly blocks a normal development plan"));
|
|
240
233
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const driftToolDefs = new Map();
|
|
245
|
-
for (const tool of fullTools) {
|
|
246
|
-
driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
|
|
234
|
+
try {
|
|
235
|
+
guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
|
|
236
|
+
results.push(failResult("save_plan security rule did not block credential access"));
|
|
247
237
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
results.push(`[OK] Schema drift check — no drift detected`);
|
|
251
|
-
ok++;
|
|
238
|
+
catch {
|
|
239
|
+
results.push(okResult("save_plan security rules loaded"));
|
|
252
240
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
241
|
+
return results;
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const checkTaskArtifactAllowlist = {
|
|
245
|
+
id: "task-artifact-allowlist",
|
|
246
|
+
description: "Read-only task artifact allowlist",
|
|
247
|
+
run() {
|
|
248
|
+
const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
|
|
249
|
+
return [
|
|
250
|
+
checkResult("Read-only task artifact allowlist", requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)), requiredReadOnlyFiles.join(", ")),
|
|
251
|
+
];
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
const checkServerVersion = {
|
|
255
|
+
id: "server-version",
|
|
256
|
+
description: "Server version and manifest preflight",
|
|
257
|
+
run() {
|
|
258
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
259
|
+
return [
|
|
260
|
+
checkResult("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION, `${PATCHWARDEN_VERSION} vs ${packageJson.version}`),
|
|
261
|
+
checkResult("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")), "scripts/checks/mcp-manifest-check.js"),
|
|
262
|
+
];
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
const checkToolProfiles = {
|
|
266
|
+
id: "tool-profiles",
|
|
267
|
+
description: "Tool profile and schema drift checks",
|
|
268
|
+
run(context) {
|
|
269
|
+
const results = [];
|
|
270
|
+
const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
271
|
+
try {
|
|
272
|
+
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
273
|
+
const fullTools = getToolDefs();
|
|
274
|
+
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", context.config?.enableDirectProfile);
|
|
275
|
+
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
|
|
276
|
+
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
|
|
277
|
+
results.push(checkResult("Full tool profile exposes 66 tools", fullTools.length === 66, `${fullTools.length} tools`));
|
|
278
|
+
results.push(checkResult(`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`, JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES), coreTools.map((tool) => tool.name).join(", ")));
|
|
279
|
+
results.push(checkResult("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
|
|
280
|
+
createSchema?.properties?.verify_commands &&
|
|
281
|
+
waitSchema?.properties?.timeout_seconds &&
|
|
282
|
+
waitSchema?.properties?.wait_seconds)));
|
|
283
|
+
// chatgpt_direct profile checks
|
|
284
|
+
const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
|
|
285
|
+
results.push(checkResult("chatgpt_direct disabled exposes only health_check (degraded mode)", directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check", `${directDisabledTools.map((t) => t.name).join(", ")}`));
|
|
286
|
+
if (context.config?.enableDirectProfile) {
|
|
287
|
+
const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
|
|
288
|
+
results.push(checkResult(`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`, JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES), directEnabledTools.map((tool) => tool.name).join(", ")));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
results.push(okResult("chatgpt_direct enabled check skipped (enableDirectProfile=false)"));
|
|
292
|
+
}
|
|
293
|
+
// Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
|
|
294
|
+
const driftRegistry = buildToolRegistry(fullTools);
|
|
295
|
+
const driftToolDefs = new Map();
|
|
296
|
+
for (const tool of fullTools) {
|
|
297
|
+
driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
|
|
298
|
+
}
|
|
299
|
+
const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
|
|
300
|
+
if (driftResult.ok) {
|
|
301
|
+
results.push(okResult("Schema drift check — no drift detected"));
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
for (const w of driftResult.warnings) {
|
|
305
|
+
results.push(warnResult(`Schema drift: ${w}`));
|
|
306
|
+
}
|
|
258
307
|
}
|
|
259
308
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
309
|
+
finally {
|
|
310
|
+
if (previousProfile === undefined)
|
|
311
|
+
delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
312
|
+
else
|
|
313
|
+
process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
|
|
314
|
+
}
|
|
315
|
+
return results;
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
// Release gate module loadable (v1.0.0) — module integrity only.
|
|
319
|
+
// Does NOT execute local_ready (would recurse into doctor:ci).
|
|
320
|
+
const checkReleaseGate = {
|
|
321
|
+
id: "release-gate",
|
|
322
|
+
description: "Release gate module loadable",
|
|
323
|
+
run() {
|
|
324
|
+
try {
|
|
325
|
+
const releaseGateReady = typeof runReleaseGateCheck === "function";
|
|
326
|
+
return [
|
|
327
|
+
checkResult("Release gate module loadable", releaseGateReady, releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid"),
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
return [checkResult("Release gate module loadable", false, error instanceof Error ? error.message : String(error))];
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
const checkHttpPort = {
|
|
336
|
+
id: "http-port",
|
|
337
|
+
description: "HTTP port check",
|
|
338
|
+
async run(context) {
|
|
339
|
+
const httpPort = context.config?.http?.port || context.config?.httpPort || 7331;
|
|
340
|
+
try {
|
|
341
|
+
const server = createServer();
|
|
342
|
+
await new Promise((resolvePort, rejectPort) => {
|
|
343
|
+
server.once("error", rejectPort);
|
|
344
|
+
server.listen(httpPort, "127.0.0.1", () => {
|
|
345
|
+
server.close();
|
|
346
|
+
resolvePort();
|
|
347
|
+
});
|
|
285
348
|
});
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
+
return [okResult(`HTTP port ${httpPort} is free`)];
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return [warnResult(`HTTP port ${httpPort} is in use — change http.port in config`)];
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
const checkDistFiles = {
|
|
357
|
+
id: "dist-files",
|
|
358
|
+
description: "dist file checks",
|
|
359
|
+
run() {
|
|
360
|
+
const distChecks = [
|
|
361
|
+
{ file: "dist/index.js", label: "stdio MCP entry", cmd: "npm run build" },
|
|
362
|
+
{ file: "dist/httpServer.js", label: "HTTP MCP entry", cmd: "npm run build" },
|
|
363
|
+
{ file: "dist/runner/watch.js", label: "watcher entry (npm run watch)", cmd: "npm run build" },
|
|
364
|
+
];
|
|
365
|
+
const results = [];
|
|
366
|
+
for (const { file, label, cmd: buildCmd } of distChecks) {
|
|
367
|
+
const exists = existsSync(resolve(process.cwd(), file));
|
|
368
|
+
results.push(checkResult(`${label} exists`, exists, exists ? file : `Missing — run: ${buildCmd}`));
|
|
369
|
+
}
|
|
370
|
+
return results;
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
const checkToolModules = {
|
|
374
|
+
id: "tool-modules",
|
|
375
|
+
description: "Tool module checks",
|
|
376
|
+
run() {
|
|
377
|
+
const newTools = [
|
|
378
|
+
"listTasks",
|
|
379
|
+
"listAgents",
|
|
380
|
+
"cancelTask",
|
|
381
|
+
"killTask",
|
|
382
|
+
"retryTask",
|
|
383
|
+
"getTaskProgress",
|
|
384
|
+
"getTaskSummary",
|
|
385
|
+
"waitForTask",
|
|
386
|
+
"getTaskStdoutTail",
|
|
387
|
+
"healthCheck",
|
|
388
|
+
"auditTask",
|
|
389
|
+
];
|
|
390
|
+
const results = [];
|
|
391
|
+
for (const t of newTools) {
|
|
392
|
+
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
393
|
+
const exists = existsSync(compiled);
|
|
394
|
+
results.push(checkResult(`Tool module: ${t}`, exists, exists ? "compiled" : "missing"));
|
|
395
|
+
}
|
|
396
|
+
return results;
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
const checkDirectToolModules = {
|
|
400
|
+
id: "direct-tool-modules",
|
|
401
|
+
description: "Direct tool module checks",
|
|
402
|
+
run() {
|
|
403
|
+
const directToolModules = [
|
|
404
|
+
"createDirectSession",
|
|
405
|
+
"searchWorkspace",
|
|
406
|
+
"applyPatch",
|
|
407
|
+
"runVerification",
|
|
408
|
+
"finalizeDirectSession",
|
|
409
|
+
"auditSession",
|
|
410
|
+
];
|
|
411
|
+
const results = [];
|
|
412
|
+
for (const t of directToolModules) {
|
|
413
|
+
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
414
|
+
const exists = existsSync(compiled);
|
|
415
|
+
results.push(checkResult(`Direct tool module: ${t}`, exists, exists ? "compiled" : "missing"));
|
|
416
|
+
}
|
|
417
|
+
return results;
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
const checkDirectSupportModules = {
|
|
421
|
+
id: "direct-support-modules",
|
|
422
|
+
description: "Direct support module checks",
|
|
423
|
+
run() {
|
|
424
|
+
const directSupportModules = [
|
|
425
|
+
"directSessionStore",
|
|
426
|
+
"directGuards",
|
|
427
|
+
"directPatch",
|
|
428
|
+
"directVerification",
|
|
429
|
+
"directAudit",
|
|
430
|
+
];
|
|
431
|
+
const results = [];
|
|
432
|
+
for (const t of directSupportModules) {
|
|
433
|
+
const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
|
|
434
|
+
const exists = existsSync(compiled);
|
|
435
|
+
results.push(checkResult(`Direct support module: ${t}`, exists, exists ? "compiled" : "missing"));
|
|
436
|
+
}
|
|
437
|
+
return results;
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
const checkTaskDirectoryWritable = {
|
|
441
|
+
id: "task-directory-writable",
|
|
442
|
+
description: "Task directory and workspaceRoot writable",
|
|
443
|
+
run(context) {
|
|
444
|
+
if (!context.config)
|
|
445
|
+
return [];
|
|
446
|
+
const results = [];
|
|
447
|
+
const tasksDir = resolve(context.config.workspaceRoot, context.config.tasksDir);
|
|
349
448
|
try {
|
|
350
449
|
mkdirSync(tasksDir, { recursive: true });
|
|
351
450
|
const testFile = join(tasksDir, ".doctor-write-test");
|
|
352
451
|
writeFileSync(testFile, "ok", "utf-8");
|
|
353
452
|
rmSync(testFile);
|
|
354
|
-
|
|
453
|
+
results.push(checkResult("Task directory writable", true, tasksDir));
|
|
355
454
|
const sampleTaskDir = join(tasksDir, ".doctor-sample-task");
|
|
356
455
|
mkdirSync(sampleTaskDir, { recursive: true });
|
|
357
456
|
const sampleStatus = join(sampleTaskDir, "status.json");
|
|
358
457
|
writeFileSync(sampleStatus, JSON.stringify({ status: "doctor" }), "utf-8");
|
|
359
458
|
const sampleReadable = JSON.parse(readFileSync(sampleStatus, "utf-8")).status === "doctor";
|
|
360
459
|
rmSync(sampleTaskDir, { recursive: true, force: true });
|
|
361
|
-
|
|
460
|
+
results.push(checkResult("Example task directory read/write", sampleReadable, sampleTaskDir));
|
|
362
461
|
}
|
|
363
462
|
catch {
|
|
364
|
-
|
|
463
|
+
results.push(warnCheckResult("Task directory writable", false, tasksDir));
|
|
365
464
|
}
|
|
366
465
|
// workspaceRoot writable
|
|
367
466
|
try {
|
|
368
|
-
const testFile = resolve(config.workspaceRoot, ".doctor-write-test");
|
|
467
|
+
const testFile = resolve(context.config.workspaceRoot, ".doctor-write-test");
|
|
369
468
|
writeFileSync(testFile, "ok", "utf-8");
|
|
370
469
|
rmSync(testFile);
|
|
371
|
-
|
|
470
|
+
results.push(checkResult("workspaceRoot writable", true, context.config.workspaceRoot));
|
|
372
471
|
}
|
|
373
472
|
catch {
|
|
374
|
-
|
|
473
|
+
results.push(warnCheckResult("workspaceRoot writable", false, context.config.workspaceRoot));
|
|
375
474
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
475
|
+
return results;
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
const checkWatcherStaleThreshold = {
|
|
479
|
+
id: "watcher-stale-threshold",
|
|
480
|
+
description: "Watcher stale threshold is valid",
|
|
481
|
+
run(context) {
|
|
482
|
+
if (!context.config)
|
|
483
|
+
return [];
|
|
484
|
+
return [
|
|
485
|
+
checkResult("Watcher stale threshold is valid", context.config.watcherStaleSeconds >= 5 && context.config.watcherStaleSeconds <= 3600, `${context.config.watcherStaleSeconds}s`),
|
|
486
|
+
];
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
const checkAllowedTestCommandsIncludesNpmTest = {
|
|
490
|
+
id: "allowed-test-commands-npm-test",
|
|
491
|
+
description: "allowedTestCommands includes npm test",
|
|
492
|
+
run(context) {
|
|
493
|
+
if (!context.config)
|
|
494
|
+
return [];
|
|
495
|
+
const hasNpmTest = context.config.allowedTestCommands.some((c) => c === "npm test" || c === "npm run test");
|
|
496
|
+
return [warnCheckResult("allowedTestCommands includes npm test", hasNpmTest, hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands")];
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
const checkTaskTimeoutDefaults = {
|
|
500
|
+
id: "task-timeout-defaults",
|
|
501
|
+
description: "Task timeout defaults are valid",
|
|
502
|
+
run(context) {
|
|
503
|
+
if (!context.config)
|
|
504
|
+
return [];
|
|
505
|
+
return [
|
|
506
|
+
checkResult("Task timeout defaults are valid", context.config.defaultTaskTimeoutSeconds > 0 && context.config.defaultTaskTimeoutSeconds <= context.config.maxTaskTimeoutSeconds, `default ${context.config.defaultTaskTimeoutSeconds}s, max ${context.config.maxTaskTimeoutSeconds}s`),
|
|
507
|
+
];
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
const checkAgentCommands = {
|
|
511
|
+
id: "agent-commands",
|
|
512
|
+
description: "Agent command check",
|
|
513
|
+
run(context) {
|
|
514
|
+
if (!context.config)
|
|
515
|
+
return [];
|
|
516
|
+
const results = [];
|
|
517
|
+
const agents = context.config.agents || {};
|
|
391
518
|
for (const [name, agentCfg] of Object.entries(agents)) {
|
|
392
519
|
const cmdName = agentCfg.command;
|
|
393
520
|
const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
|
|
394
521
|
if (looksLikePath) {
|
|
395
522
|
const agentExists = existsSync(cmdName);
|
|
396
|
-
|
|
523
|
+
results.push(warnCheckResult(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`));
|
|
397
524
|
continue;
|
|
398
525
|
}
|
|
399
526
|
// Platform-appropriate lookup: 'where' on Windows, 'command -v' on Unix
|
|
@@ -401,69 +528,171 @@ async function main() {
|
|
|
401
528
|
const lookupCmd = isWin ? `where ${cmdName}` : `command -v ${cmdName}`;
|
|
402
529
|
const fallbackCmd = isWin ? `command -v ${cmdName}` : `which ${cmdName}`;
|
|
403
530
|
const found = cmd(lookupCmd) || cmd(fallbackCmd);
|
|
404
|
-
|
|
531
|
+
results.push(warnCheckResult(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`));
|
|
405
532
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
533
|
+
return results;
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const checkAllowedTestCommandsSafety = {
|
|
537
|
+
id: "allowed-test-commands-safety",
|
|
538
|
+
description: "allowedTestCommands safety check",
|
|
539
|
+
run(context) {
|
|
540
|
+
if (!context.config)
|
|
541
|
+
return [];
|
|
542
|
+
const results = [];
|
|
543
|
+
const testCmds = context.config.allowedTestCommands || [];
|
|
544
|
+
results.push(checkResult("allowedTestCommands is non-empty", testCmds.length > 0, testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured"));
|
|
411
545
|
const dangerous = ["rm -rf", "del /s", "format", "shutdown", "curl |", "wget |"];
|
|
412
546
|
for (const cmdStr of testCmds) {
|
|
413
547
|
for (const danger of dangerous) {
|
|
414
548
|
if (cmdStr.toLowerCase().includes(danger)) {
|
|
415
|
-
results.push(`
|
|
416
|
-
warn++;
|
|
549
|
+
results.push(warnResult(`allowedTestCommands contains dangerous pattern: "${cmdStr}"`));
|
|
417
550
|
}
|
|
418
551
|
}
|
|
419
552
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
553
|
+
return results;
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
// Direct profile config checks
|
|
557
|
+
const checkDirectProfileConfig = {
|
|
558
|
+
id: "direct-profile-config",
|
|
559
|
+
description: "Direct profile config checks",
|
|
560
|
+
run(context) {
|
|
561
|
+
if (!context.config)
|
|
562
|
+
return [];
|
|
563
|
+
const results = [];
|
|
564
|
+
const directCmds = context.config.directAllowedCommands || [];
|
|
565
|
+
results.push(warnCheckResult("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured"));
|
|
425
566
|
// npm run doctor should NOT be in default Direct whitelist
|
|
426
567
|
const hasDoctor = directCmds.some((c) => c === "npm run doctor");
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
568
|
+
results.push(checkResult("directAllowedCommands does not include npm run doctor", !hasDoctor, hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present"));
|
|
569
|
+
results.push(checkResult("directSessionTtlSeconds is valid", context.config.directSessionTtlSeconds >= 60 && context.config.directSessionTtlSeconds <= 86400, `${context.config.directSessionTtlSeconds}s`));
|
|
570
|
+
results.push(checkResult("directMaxPatchBytes is positive", context.config.directMaxPatchBytes > 0, `${context.config.directMaxPatchBytes}`));
|
|
571
|
+
results.push(checkResult("directMaxFileBytes is positive", context.config.directMaxFileBytes > 0, `${context.config.directMaxFileBytes}`));
|
|
572
|
+
return results;
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
// Tunnel example files check
|
|
576
|
+
const checkTunnelExamples = {
|
|
577
|
+
id: "tunnel-examples",
|
|
578
|
+
description: "Tunnel example files check",
|
|
579
|
+
run() {
|
|
580
|
+
const tunnelFiles = [
|
|
581
|
+
"examples/openai-tunnel/README.md",
|
|
582
|
+
"examples/openai-tunnel/tunnel-client.example.yaml",
|
|
583
|
+
"examples/openai-tunnel/chatgpt-test-prompt.md",
|
|
584
|
+
"scripts/mcp/patchwarden-mcp-direct.cmd",
|
|
585
|
+
"PatchWarden.cmd",
|
|
586
|
+
"scripts/control/manage-patchwarden.ps1",
|
|
587
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
588
|
+
];
|
|
589
|
+
const results = [];
|
|
590
|
+
for (const tf of tunnelFiles) {
|
|
591
|
+
const full = resolve(process.cwd(), tf);
|
|
592
|
+
const exists = existsSync(full);
|
|
593
|
+
results.push(checkResult(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing"));
|
|
594
|
+
// Check for leaked secrets in example files
|
|
595
|
+
if (exists) {
|
|
596
|
+
const content = readFileSync(full, "utf-8");
|
|
597
|
+
// Only flag actual key-value assignments, not comments/mentions
|
|
598
|
+
const leaked = /(?:api_key|sk-[a-zA-Z0-9]{10,}|token\s*[:=]\s*\S{4,}|secret\s*[:=]\s*\S{4,}|password\s*[:=]\s*\S{4,})/gi.test(
|
|
599
|
+
// Strip comment lines first
|
|
600
|
+
content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n"));
|
|
601
|
+
if (leaked) {
|
|
602
|
+
results.push(warnResult(`${tf} may contain secrets`));
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
results.push(okResult(`${tf} — no real secrets`));
|
|
606
|
+
}
|
|
460
607
|
}
|
|
461
608
|
}
|
|
609
|
+
return results;
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
// ── Check registry ─────────────────────────────────────────────────
|
|
613
|
+
const checks = [
|
|
614
|
+
checkNodeVersion,
|
|
615
|
+
checkNpmAvailable,
|
|
616
|
+
checkConfirmPackage,
|
|
617
|
+
checkGitAvailable,
|
|
618
|
+
checkConfigFile,
|
|
619
|
+
checkPatchwardenConfigEnv,
|
|
620
|
+
checkConfigParseable,
|
|
621
|
+
checkWorkspaceRoot,
|
|
622
|
+
checkPathGuard,
|
|
623
|
+
checkSensitiveGuard,
|
|
624
|
+
checkSavePlanRules,
|
|
625
|
+
checkTaskArtifactAllowlist,
|
|
626
|
+
checkServerVersion,
|
|
627
|
+
checkToolProfiles,
|
|
628
|
+
checkReleaseGate,
|
|
629
|
+
checkHttpPort,
|
|
630
|
+
checkDistFiles,
|
|
631
|
+
checkToolModules,
|
|
632
|
+
checkDirectToolModules,
|
|
633
|
+
checkDirectSupportModules,
|
|
634
|
+
checkTaskDirectoryWritable,
|
|
635
|
+
checkWatcherStaleThreshold,
|
|
636
|
+
checkAllowedTestCommandsIncludesNpmTest,
|
|
637
|
+
checkTaskTimeoutDefaults,
|
|
638
|
+
checkAgentCommands,
|
|
639
|
+
checkAllowedTestCommandsSafety,
|
|
640
|
+
checkDirectProfileConfig,
|
|
641
|
+
checkTunnelExamples,
|
|
642
|
+
];
|
|
643
|
+
// ── Context preparation ────────────────────────────────────────────
|
|
644
|
+
function prepareContext() {
|
|
645
|
+
const allowDefaultConfig = process.argv.includes("--allow-default-config");
|
|
646
|
+
const configPaths = [
|
|
647
|
+
resolve(process.cwd(), "patchwarden.config.json"),
|
|
648
|
+
process.env.PATCHWARDEN_CONFIG || "",
|
|
649
|
+
].filter(Boolean);
|
|
650
|
+
let configPathUsed = "";
|
|
651
|
+
for (const p of configPaths) {
|
|
652
|
+
if (existsSync(p)) {
|
|
653
|
+
configPathUsed = p;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Load config (may fail)
|
|
658
|
+
let config = null;
|
|
659
|
+
let configError = null;
|
|
660
|
+
try {
|
|
661
|
+
config = getConfig();
|
|
662
|
+
}
|
|
663
|
+
catch (err) {
|
|
664
|
+
configError = err instanceof Error ? err.message : String(err);
|
|
665
|
+
}
|
|
666
|
+
return { config, configError, allowDefaultConfig, configPathUsed };
|
|
667
|
+
}
|
|
668
|
+
// ══════════════════════════════════════════════════════════════════
|
|
669
|
+
// Main
|
|
670
|
+
// ══════════════════════════════════════════════════════════════════
|
|
671
|
+
async function main() {
|
|
672
|
+
console.log("PatchWarden Doctor\n");
|
|
673
|
+
const context = prepareContext();
|
|
674
|
+
const allResults = [];
|
|
675
|
+
for (const check of checks) {
|
|
676
|
+
try {
|
|
677
|
+
const results = await check.run(context);
|
|
678
|
+
allResults.push(...results);
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
allResults.push(failResult(check.description, err instanceof Error ? err.message : String(err)));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
console.log(allResults.map(formatResult).join("\n"));
|
|
685
|
+
let ok = 0;
|
|
686
|
+
let warn = 0;
|
|
687
|
+
let fail = 0;
|
|
688
|
+
for (const r of allResults) {
|
|
689
|
+
if (r.level === "OK")
|
|
690
|
+
ok++;
|
|
691
|
+
else if (r.level === "WARN")
|
|
692
|
+
warn++;
|
|
693
|
+
else
|
|
694
|
+
fail++;
|
|
462
695
|
}
|
|
463
|
-
// ══════════════════════════════════════════════════════════════════
|
|
464
|
-
// Summary
|
|
465
|
-
// ══════════════════════════════════════════════════════════════════
|
|
466
|
-
console.log(results.join("\n"));
|
|
467
696
|
console.log(`\n${"=".repeat(50)}`);
|
|
468
697
|
console.log(`OK: ${ok} WARN: ${warn} FAIL: ${fail}`);
|
|
469
698
|
console.log(`${"=".repeat(50)}`);
|
|
@@ -480,8 +709,10 @@ async function main() {
|
|
|
480
709
|
console.log("\n✅ All checks passed.");
|
|
481
710
|
process.exit(0);
|
|
482
711
|
}
|
|
483
|
-
}
|
|
712
|
+
}
|
|
484
713
|
main().catch((err) => {
|
|
485
|
-
|
|
714
|
+
logger.fatal("Doctor crashed", {
|
|
715
|
+
error: err instanceof Error ? err.message : String(err),
|
|
716
|
+
});
|
|
486
717
|
process.exit(1);
|
|
487
718
|
});
|