patchwarden 0.4.0
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/Check-PatchWarden-Health.cmd +6 -0
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/Restart-PatchWarden.cmd +19 -0
- package/Start-PatchWarden-Tunnel.cmd +7 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +131 -0
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +388 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +27 -0
- package/dist/httpServer.d.ts +17 -0
- package/dist/httpServer.js +255 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +24 -0
- package/dist/runner/changeCapture.d.ts +43 -0
- package/dist/runner/changeCapture.js +245 -0
- package/dist/runner/cli.d.ts +9 -0
- package/dist/runner/cli.js +26 -0
- package/dist/runner/runTask.d.ts +8 -0
- package/dist/runner/runTask.js +821 -0
- package/dist/runner/watch.d.ts +19 -0
- package/dist/runner/watch.js +136 -0
- package/dist/security/commandGuard.d.ts +22 -0
- package/dist/security/commandGuard.js +56 -0
- package/dist/security/contentRedaction.d.ts +12 -0
- package/dist/security/contentRedaction.js +65 -0
- package/dist/security/pathGuard.d.ts +3 -0
- package/dist/security/pathGuard.js +98 -0
- package/dist/security/planGuard.d.ts +1 -0
- package/dist/security/planGuard.js +65 -0
- package/dist/security/sensitiveGuard.d.ts +2 -0
- package/dist/security/sensitiveGuard.js +60 -0
- package/dist/smoke-test.d.ts +15 -0
- package/dist/smoke-test.js +989 -0
- package/dist/taskProgress.d.ts +6 -0
- package/dist/taskProgress.js +36 -0
- package/dist/taskRuntime.d.ts +10 -0
- package/dist/taskRuntime.js +25 -0
- package/dist/tools/auditTask.d.ts +19 -0
- package/dist/tools/auditTask.js +307 -0
- package/dist/tools/cancelTask.d.ts +30 -0
- package/dist/tools/cancelTask.js +64 -0
- package/dist/tools/createTask.d.ts +42 -0
- package/dist/tools/createTask.js +188 -0
- package/dist/tools/getPlan.d.ts +12 -0
- package/dist/tools/getPlan.js +29 -0
- package/dist/tools/getTaskFile.d.ts +24 -0
- package/dist/tools/getTaskFile.js +84 -0
- package/dist/tools/getTaskProgress.d.ts +1 -0
- package/dist/tools/getTaskProgress.js +4 -0
- package/dist/tools/getTaskStatus.d.ts +46 -0
- package/dist/tools/getTaskStatus.js +36 -0
- package/dist/tools/getTaskStdoutTail.d.ts +9 -0
- package/dist/tools/getTaskStdoutTail.js +56 -0
- package/dist/tools/getTaskSummary.d.ts +66 -0
- package/dist/tools/getTaskSummary.js +237 -0
- package/dist/tools/healthCheck.d.ts +79 -0
- package/dist/tools/healthCheck.js +230 -0
- package/dist/tools/killTask.d.ts +15 -0
- package/dist/tools/killTask.js +4 -0
- package/dist/tools/listAgents.d.ts +12 -0
- package/dist/tools/listAgents.js +42 -0
- package/dist/tools/listTasks.d.ts +36 -0
- package/dist/tools/listTasks.js +94 -0
- package/dist/tools/listWorkspace.d.ts +10 -0
- package/dist/tools/listWorkspace.js +54 -0
- package/dist/tools/readWorkspaceFile.d.ts +8 -0
- package/dist/tools/readWorkspaceFile.js +26 -0
- package/dist/tools/registry.d.ts +24 -0
- package/dist/tools/registry.js +566 -0
- package/dist/tools/retryTask.d.ts +8 -0
- package/dist/tools/retryTask.js +45 -0
- package/dist/tools/savePlan.d.ts +10 -0
- package/dist/tools/savePlan.js +28 -0
- package/dist/tools/taskOutputs.d.ts +49 -0
- package/dist/tools/taskOutputs.js +136 -0
- package/dist/tools/taskTemplates.d.ts +15 -0
- package/dist/tools/taskTemplates.js +116 -0
- package/dist/tools/toolCatalog.d.ts +19 -0
- package/dist/tools/toolCatalog.js +71 -0
- package/dist/tools/waitForTask.d.ts +25 -0
- package/dist/tools/waitForTask.js +59 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/watcherStatus.d.ts +20 -0
- package/dist/watcherStatus.js +77 -0
- package/docs/assets/patchwarden-chatgpt-demo.svg +30 -0
- package/docs/demo.md +58 -0
- package/docs/migration-from-safe-bifrost.md +31 -0
- package/docs/release-v0.3.0.md +43 -0
- package/docs/release-v0.4.0.md +74 -0
- package/examples/config.example.json +40 -0
- package/examples/openai-tunnel/README.md +48 -0
- package/examples/openai-tunnel/chatgpt-test-prompt.md +53 -0
- package/examples/openai-tunnel/tunnel-client.example.yaml +39 -0
- package/package.json +73 -0
- package/scripts/brand-check.js +44 -0
- package/scripts/doctor-smoke.js +52 -0
- package/scripts/get-patchwarden-health.ps1 +114 -0
- package/scripts/http-mcp-smoke.js +351 -0
- package/scripts/lifecycle-smoke.js +453 -0
- package/scripts/mcp-manifest-check.js +120 -0
- package/scripts/mcp-smoke.js +336 -0
- package/scripts/pack-clean.js +142 -0
- package/scripts/patchwarden-mcp-stdio.cmd +7 -0
- package/scripts/restart-patchwarden.ps1 +242 -0
- package/scripts/start-patchwarden-tunnel.ps1 +503 -0
- package/scripts/tunnel-supervisor-smoke.js +77 -0
- package/scripts/watcher-supervisor-smoke.js +195 -0
- package/src/config.ts +165 -0
- package/src/doctor.ts +438 -0
- package/src/errors.ts +26 -0
- package/src/httpServer.ts +277 -0
- package/src/index.ts +34 -0
- package/src/runner/changeCapture.ts +304 -0
- package/src/runner/cli.ts +34 -0
- package/src/runner/runTask.ts +935 -0
- package/src/runner/watch.ts +152 -0
- package/src/security/commandGuard.ts +97 -0
- package/src/security/contentRedaction.ts +85 -0
- package/src/security/pathGuard.ts +175 -0
- package/src/security/planGuard.ts +93 -0
- package/src/security/sensitiveGuard.ts +72 -0
- package/src/smoke-test.ts +1078 -0
- package/src/taskProgress.ts +41 -0
- package/src/taskRuntime.ts +37 -0
- package/src/tools/auditTask.ts +340 -0
- package/src/tools/cancelTask.ts +70 -0
- package/src/tools/createTask.ts +337 -0
- package/src/tools/getPlan.ts +48 -0
- package/src/tools/getTaskFile.ts +111 -0
- package/src/tools/getTaskProgress.ts +5 -0
- package/src/tools/getTaskStatus.ts +85 -0
- package/src/tools/getTaskStdoutTail.ts +61 -0
- package/src/tools/getTaskSummary.ts +307 -0
- package/src/tools/healthCheck.ts +234 -0
- package/src/tools/killTask.ts +5 -0
- package/src/tools/listAgents.ts +54 -0
- package/src/tools/listTasks.ts +139 -0
- package/src/tools/listWorkspace.ts +71 -0
- package/src/tools/readWorkspaceFile.ts +41 -0
- package/src/tools/registry.ts +666 -0
- package/src/tools/retryTask.ts +53 -0
- package/src/tools/savePlan.ts +46 -0
- package/src/tools/taskOutputs.ts +155 -0
- package/src/tools/taskTemplates.ts +162 -0
- package/src/tools/toolCatalog.ts +92 -0
- package/src/tools/waitForTask.ts +90 -0
- package/src/version.ts +2 -0
- package/src/watcherStatus.ts +102 -0
- package/tsconfig.json +17 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
// ── Defaults ──────────────────────────────────────────────────────
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
workspaceRoot: process.cwd(),
|
|
6
|
+
plansDir: ".patchwarden/plans",
|
|
7
|
+
tasksDir: ".patchwarden/tasks",
|
|
8
|
+
agents: {
|
|
9
|
+
codex: {
|
|
10
|
+
command: "codex",
|
|
11
|
+
args: ["exec", "--cd", "{repo}", "{prompt}"],
|
|
12
|
+
},
|
|
13
|
+
opencode: {
|
|
14
|
+
command: "opencode",
|
|
15
|
+
args: ["run", "{prompt}"],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
allowedTestCommands: [
|
|
19
|
+
"npm test",
|
|
20
|
+
"npm run test",
|
|
21
|
+
"npm run lint",
|
|
22
|
+
"npm run format:check",
|
|
23
|
+
"npm run build",
|
|
24
|
+
"npm run dist",
|
|
25
|
+
"npm run doctor",
|
|
26
|
+
"pnpm test",
|
|
27
|
+
"pnpm run test",
|
|
28
|
+
"pnpm run lint",
|
|
29
|
+
"pnpm run format:check",
|
|
30
|
+
"pnpm run build",
|
|
31
|
+
"pnpm run dist",
|
|
32
|
+
"pnpm run doctor",
|
|
33
|
+
"pytest",
|
|
34
|
+
"cargo test",
|
|
35
|
+
],
|
|
36
|
+
maxReadFileBytes: 200_000,
|
|
37
|
+
defaultTaskTimeoutSeconds: 900,
|
|
38
|
+
maxTaskTimeoutSeconds: 3600,
|
|
39
|
+
watcherStaleSeconds: 30,
|
|
40
|
+
toolProfile: "full",
|
|
41
|
+
};
|
|
42
|
+
// ── Load config ───────────────────────────────────────────────────
|
|
43
|
+
let _config = null;
|
|
44
|
+
export function loadConfig(configPath) {
|
|
45
|
+
if (_config)
|
|
46
|
+
return _config;
|
|
47
|
+
const explicitPath = configPath || process.env.PATCHWARDEN_CONFIG;
|
|
48
|
+
const candidatePaths = explicitPath
|
|
49
|
+
? [explicitPath]
|
|
50
|
+
: [
|
|
51
|
+
resolve(process.cwd(), "patchwarden.config.json"),
|
|
52
|
+
resolve(process.cwd(), ".patchwarden.json"),
|
|
53
|
+
];
|
|
54
|
+
for (const p of candidatePaths) {
|
|
55
|
+
if (existsSync(p)) {
|
|
56
|
+
try {
|
|
57
|
+
const rawText = stripBom(readFileSync(p, "utf-8"));
|
|
58
|
+
const raw = JSON.parse(rawText);
|
|
59
|
+
_config = normalizeConfig({ ...DEFAULT_CONFIG, ...raw });
|
|
60
|
+
return _config;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
throw new Error(`Failed to load PatchWarden config "${p}": ${message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (explicitPath) {
|
|
69
|
+
throw new Error(`PatchWarden config not found: "${explicitPath}"`);
|
|
70
|
+
}
|
|
71
|
+
_config = normalizeConfig({ ...DEFAULT_CONFIG });
|
|
72
|
+
return _config;
|
|
73
|
+
}
|
|
74
|
+
export function getConfig() {
|
|
75
|
+
if (!_config)
|
|
76
|
+
return loadConfig();
|
|
77
|
+
return _config;
|
|
78
|
+
}
|
|
79
|
+
/** Resolve workspaceRoot: expand relative paths */
|
|
80
|
+
export function resolveWorkspaceRoot(config) {
|
|
81
|
+
return resolve(config.workspaceRoot);
|
|
82
|
+
}
|
|
83
|
+
/** Resolve plans/tasks dirs relative to workspaceRoot */
|
|
84
|
+
export function getPlansDir(config) {
|
|
85
|
+
return resolve(config.workspaceRoot, config.plansDir);
|
|
86
|
+
}
|
|
87
|
+
export function getTasksDir(config) {
|
|
88
|
+
return resolve(config.workspaceRoot, config.tasksDir);
|
|
89
|
+
}
|
|
90
|
+
function stripBom(value) {
|
|
91
|
+
return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value;
|
|
92
|
+
}
|
|
93
|
+
function normalizeConfig(config) {
|
|
94
|
+
if (!config.workspaceRoot || typeof config.workspaceRoot !== "string") {
|
|
95
|
+
throw new Error("workspaceRoot must be a non-empty string");
|
|
96
|
+
}
|
|
97
|
+
if (!config.plansDir || typeof config.plansDir !== "string") {
|
|
98
|
+
throw new Error("plansDir must be a non-empty string");
|
|
99
|
+
}
|
|
100
|
+
if (!config.tasksDir || typeof config.tasksDir !== "string") {
|
|
101
|
+
throw new Error("tasksDir must be a non-empty string");
|
|
102
|
+
}
|
|
103
|
+
if (!config.agents || typeof config.agents !== "object") {
|
|
104
|
+
throw new Error("agents must be an object");
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(config.allowedTestCommands)) {
|
|
107
|
+
throw new Error("allowedTestCommands must be an array");
|
|
108
|
+
}
|
|
109
|
+
if (!Number.isFinite(config.maxReadFileBytes) || config.maxReadFileBytes <= 0) {
|
|
110
|
+
throw new Error("maxReadFileBytes must be a positive number");
|
|
111
|
+
}
|
|
112
|
+
if (!Number.isInteger(config.defaultTaskTimeoutSeconds) || config.defaultTaskTimeoutSeconds <= 0) {
|
|
113
|
+
throw new Error("defaultTaskTimeoutSeconds must be a positive integer");
|
|
114
|
+
}
|
|
115
|
+
if (!Number.isInteger(config.maxTaskTimeoutSeconds) || config.maxTaskTimeoutSeconds <= 0) {
|
|
116
|
+
throw new Error("maxTaskTimeoutSeconds must be a positive integer");
|
|
117
|
+
}
|
|
118
|
+
if (config.defaultTaskTimeoutSeconds > config.maxTaskTimeoutSeconds) {
|
|
119
|
+
throw new Error("defaultTaskTimeoutSeconds cannot exceed maxTaskTimeoutSeconds");
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isInteger(config.watcherStaleSeconds) || config.watcherStaleSeconds < 5 || config.watcherStaleSeconds > 3600) {
|
|
122
|
+
throw new Error("watcherStaleSeconds must be an integer from 5 to 3600");
|
|
123
|
+
}
|
|
124
|
+
if (config.toolProfile !== undefined && config.toolProfile !== "full" && config.toolProfile !== "chatgpt_core") {
|
|
125
|
+
throw new Error('toolProfile must be "full" or "chatgpt_core"');
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...config,
|
|
129
|
+
workspaceRoot: resolve(config.workspaceRoot),
|
|
130
|
+
};
|
|
131
|
+
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PatchWarden Doctor — read-only diagnostic checks
|
|
4
|
+
*
|
|
5
|
+
* Usage: node dist/doctor.js or npm run doctor
|
|
6
|
+
*
|
|
7
|
+
* Checks 15 aspects of the environment and configuration.
|
|
8
|
+
* Never modifies files, installs dependencies, or starts services.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PatchWarden Doctor — read-only diagnostic checks
|
|
4
|
+
*
|
|
5
|
+
* Usage: node dist/doctor.js or npm run doctor
|
|
6
|
+
*
|
|
7
|
+
* Checks 15 aspects of the environment and configuration.
|
|
8
|
+
* Never modifies files, installs dependencies, or starts services.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, statSync, readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { isAbsolute, resolve, normalize, join } from "node:path";
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { createServer } from "node:net";
|
|
14
|
+
import { getConfig } from "./config.js";
|
|
15
|
+
import { guardPath, guardWorkspacePath } from "./security/pathGuard.js";
|
|
16
|
+
import { isSensitivePath } from "./security/sensitiveGuard.js";
|
|
17
|
+
import { guardPlanContent } from "./security/planGuard.js";
|
|
18
|
+
import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
|
|
19
|
+
import { getToolDefs } from "./tools/registry.js";
|
|
20
|
+
import { CHATGPT_CORE_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
21
|
+
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
22
|
+
// ── State ──────────────────────────────────────────────────────────
|
|
23
|
+
let ok = 0;
|
|
24
|
+
let warn = 0;
|
|
25
|
+
let fail = 0;
|
|
26
|
+
const results = [];
|
|
27
|
+
function check(name, condition, detail) {
|
|
28
|
+
if (condition) {
|
|
29
|
+
results.push(`[OK] ${name}`);
|
|
30
|
+
ok++;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
results.push(`[FAIL] ${name}${detail ? " — " + detail : ""}`);
|
|
34
|
+
fail++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function warnCheck(name, condition, detail) {
|
|
38
|
+
if (condition) {
|
|
39
|
+
results.push(`[OK] ${name}`);
|
|
40
|
+
ok++;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
results.push(`[WARN] ${name}${detail ? " — " + detail : ""}`);
|
|
44
|
+
warn++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function cmd(cmdStr) {
|
|
48
|
+
try {
|
|
49
|
+
return execSync(cmdStr, {
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
timeout: 5000,
|
|
52
|
+
stdio: ["ignore", "pipe", "ignore"], // suppress stdin and stderr
|
|
53
|
+
}).trim();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ══════════════════════════════════════════════════════════════════
|
|
60
|
+
async function main() {
|
|
61
|
+
console.log("PatchWarden Doctor\n");
|
|
62
|
+
// 1. Node version
|
|
63
|
+
const nodeVer = process.version;
|
|
64
|
+
const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
|
|
65
|
+
check("Node.js version", nodeMajor >= 18, nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`);
|
|
66
|
+
// 2. npm
|
|
67
|
+
const npmVer = cmd("npm --version");
|
|
68
|
+
check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
|
|
69
|
+
// 3. Git
|
|
70
|
+
const gitVer = cmd("git --version");
|
|
71
|
+
warnCheck("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work");
|
|
72
|
+
// 4. Config file exists
|
|
73
|
+
const configPaths = [
|
|
74
|
+
resolve(process.cwd(), "patchwarden.config.json"),
|
|
75
|
+
process.env.PATCHWARDEN_CONFIG || "",
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
let configPathUsed = "";
|
|
78
|
+
for (const p of configPaths) {
|
|
79
|
+
if (existsSync(p)) {
|
|
80
|
+
configPathUsed = p;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
check("Config file exists", configPathUsed !== "", configPathUsed
|
|
85
|
+
? configPathUsed
|
|
86
|
+
: 'Create one: cp examples/config.example.json patchwarden.config.json');
|
|
87
|
+
// 5. PATCHWARDEN_CONFIG env
|
|
88
|
+
if (process.env.PATCHWARDEN_CONFIG) {
|
|
89
|
+
results.push(`[OK] PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`);
|
|
90
|
+
ok++;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
results.push(`[OK] PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)`);
|
|
94
|
+
ok++;
|
|
95
|
+
}
|
|
96
|
+
// Load config (may fail)
|
|
97
|
+
let config = null;
|
|
98
|
+
try {
|
|
99
|
+
config = getConfig();
|
|
100
|
+
check("Config parseable", true, `workspaceRoot: ${config.workspaceRoot}`);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
check("Config parseable", false, err instanceof Error ? err.message : String(err));
|
|
104
|
+
}
|
|
105
|
+
// 6. workspaceRoot checks
|
|
106
|
+
if (config) {
|
|
107
|
+
const ws = normalize(resolve(config.workspaceRoot));
|
|
108
|
+
const exists = existsSync(ws);
|
|
109
|
+
check("workspaceRoot exists", exists, ws);
|
|
110
|
+
let isDir = false;
|
|
111
|
+
try {
|
|
112
|
+
isDir = statSync(ws).isDirectory();
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
check("workspaceRoot is directory", isDir, ws);
|
|
116
|
+
// Danger checks
|
|
117
|
+
const dangerousRoots = [
|
|
118
|
+
{ pattern: /^[A-Za-z]:\\?$/, label: "drive root" },
|
|
119
|
+
{ pattern: /\\Users\\[^\\]+$/, label: "user home directory" },
|
|
120
|
+
{ pattern: /\\Desktop$/, label: "Desktop" },
|
|
121
|
+
{ pattern: /\\Downloads$/, label: "Downloads" },
|
|
122
|
+
{ pattern: /\\Documents$/, label: "Documents" },
|
|
123
|
+
];
|
|
124
|
+
for (const { pattern, label } of dangerousRoots) {
|
|
125
|
+
const matches = pattern.test(ws);
|
|
126
|
+
if (matches) {
|
|
127
|
+
results.push(`[WARN] workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`);
|
|
128
|
+
warn++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 7. Path guard test
|
|
133
|
+
if (config) {
|
|
134
|
+
try {
|
|
135
|
+
guardPath("test-file.txt", config.workspaceRoot);
|
|
136
|
+
results.push(`[OK] pathGuard allows workspace-internal path`);
|
|
137
|
+
ok++;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
results.push(`[FAIL] pathGuard rejects internal path: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
fail++;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
guardPath("../outside", config.workspaceRoot);
|
|
145
|
+
results.push(`[FAIL] pathGuard should have blocked ../escape`);
|
|
146
|
+
fail++;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
results.push(`[OK] pathGuard blocks ../ path escape`);
|
|
150
|
+
ok++;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const relativeRepo = guardWorkspacePath(".", config.workspaceRoot);
|
|
154
|
+
const absoluteRepo = guardWorkspacePath(config.workspaceRoot, config.workspaceRoot);
|
|
155
|
+
check("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
check("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 8. Sensitive file guard test
|
|
162
|
+
const sensitivePaths = [".env", ".ssh/id_rsa", "token.json", "credentials"];
|
|
163
|
+
for (const sp of sensitivePaths) {
|
|
164
|
+
const blocked = isSensitivePath(sp);
|
|
165
|
+
if (blocked) {
|
|
166
|
+
results.push(`[OK] sensitiveGuard blocks "${sp}"`);
|
|
167
|
+
ok++;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
results.push(`[FAIL] sensitiveGuard does NOT block "${sp}"`);
|
|
171
|
+
fail++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
|
|
176
|
+
results.push("[OK] save_plan allows normal development plans");
|
|
177
|
+
ok++;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
results.push("[FAIL] save_plan incorrectly blocks a normal development plan");
|
|
181
|
+
fail++;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
|
|
185
|
+
results.push("[FAIL] save_plan security rule did not block credential access");
|
|
186
|
+
fail++;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
results.push("[OK] save_plan security rules loaded");
|
|
190
|
+
ok++;
|
|
191
|
+
}
|
|
192
|
+
const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
|
|
193
|
+
check("Read-only task artifact allowlist", requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)), requiredReadOnlyFiles.join(", "));
|
|
194
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
195
|
+
check("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION, `${PATCHWARDEN_VERSION} vs ${packageJson.version}`);
|
|
196
|
+
check("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/mcp-manifest-check.js")), "scripts/mcp-manifest-check.js");
|
|
197
|
+
const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
198
|
+
try {
|
|
199
|
+
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
200
|
+
const fullTools = getToolDefs();
|
|
201
|
+
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core");
|
|
202
|
+
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
|
|
203
|
+
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
|
|
204
|
+
check("Full tool profile exposes 22 tools", fullTools.length === 22, `${fullTools.length} tools`);
|
|
205
|
+
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(", "));
|
|
206
|
+
check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
|
|
207
|
+
createSchema?.properties?.verify_commands &&
|
|
208
|
+
waitSchema?.properties?.timeout_seconds &&
|
|
209
|
+
waitSchema?.properties?.wait_seconds));
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
if (previousProfile === undefined)
|
|
213
|
+
delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
214
|
+
else
|
|
215
|
+
process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
|
|
216
|
+
}
|
|
217
|
+
// 9. HTTP port check
|
|
218
|
+
const httpPort = config?.http?.port || 7331;
|
|
219
|
+
try {
|
|
220
|
+
const server = createServer();
|
|
221
|
+
await new Promise((resolvePort, rejectPort) => {
|
|
222
|
+
server.once("error", rejectPort);
|
|
223
|
+
server.listen(httpPort, "127.0.0.1", () => {
|
|
224
|
+
server.close();
|
|
225
|
+
resolvePort();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
results.push(`[OK] HTTP port ${httpPort} is free`);
|
|
229
|
+
ok++;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
results.push(`[WARN] HTTP port ${httpPort} is in use — change http.port in config`);
|
|
233
|
+
warn++;
|
|
234
|
+
}
|
|
235
|
+
// 10-12. dist file checks
|
|
236
|
+
const distChecks = [
|
|
237
|
+
{ file: "dist/index.js", label: "stdio MCP entry", cmd: "npm run build" },
|
|
238
|
+
{ file: "dist/httpServer.js", label: "HTTP MCP entry", cmd: "npm run build" },
|
|
239
|
+
{ file: "dist/runner/watch.js", label: "watcher entry (npm run watch)", cmd: "npm run build" },
|
|
240
|
+
];
|
|
241
|
+
for (const { file, label, cmd: buildCmd } of distChecks) {
|
|
242
|
+
check(`${label} exists`, existsSync(resolve(process.cwd(), file)), existsSync(resolve(process.cwd(), file)) ? file : `Missing — run: ${buildCmd}`);
|
|
243
|
+
}
|
|
244
|
+
// New tool registrations check
|
|
245
|
+
const newTools = [
|
|
246
|
+
"listTasks",
|
|
247
|
+
"listAgents",
|
|
248
|
+
"cancelTask",
|
|
249
|
+
"killTask",
|
|
250
|
+
"retryTask",
|
|
251
|
+
"getTaskProgress",
|
|
252
|
+
"getTaskSummary",
|
|
253
|
+
"waitForTask",
|
|
254
|
+
"getTaskStdoutTail",
|
|
255
|
+
"healthCheck",
|
|
256
|
+
"auditTask",
|
|
257
|
+
];
|
|
258
|
+
for (const t of newTools) {
|
|
259
|
+
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
260
|
+
check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
261
|
+
}
|
|
262
|
+
// Task directory writable
|
|
263
|
+
if (config) {
|
|
264
|
+
const tasksDir = resolve(config.workspaceRoot, config.tasksDir);
|
|
265
|
+
try {
|
|
266
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
267
|
+
const testFile = join(tasksDir, ".doctor-write-test");
|
|
268
|
+
writeFileSync(testFile, "ok", "utf-8");
|
|
269
|
+
rmSync(testFile);
|
|
270
|
+
check("Task directory writable", true, tasksDir);
|
|
271
|
+
const sampleTaskDir = join(tasksDir, ".doctor-sample-task");
|
|
272
|
+
mkdirSync(sampleTaskDir, { recursive: true });
|
|
273
|
+
const sampleStatus = join(sampleTaskDir, "status.json");
|
|
274
|
+
writeFileSync(sampleStatus, JSON.stringify({ status: "doctor" }), "utf-8");
|
|
275
|
+
const sampleReadable = JSON.parse(readFileSync(sampleStatus, "utf-8")).status === "doctor";
|
|
276
|
+
rmSync(sampleTaskDir, { recursive: true, force: true });
|
|
277
|
+
check("Example task directory read/write", sampleReadable, sampleTaskDir);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
warnCheck("Task directory writable", false, tasksDir);
|
|
281
|
+
}
|
|
282
|
+
// workspaceRoot writable
|
|
283
|
+
try {
|
|
284
|
+
const testFile = resolve(config.workspaceRoot, ".doctor-write-test");
|
|
285
|
+
writeFileSync(testFile, "ok", "utf-8");
|
|
286
|
+
rmSync(testFile);
|
|
287
|
+
check("workspaceRoot writable", true, config.workspaceRoot);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
warnCheck("workspaceRoot writable", false, config.workspaceRoot);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (config) {
|
|
294
|
+
check("Watcher stale threshold is valid", config.watcherStaleSeconds >= 5 && config.watcherStaleSeconds <= 3600, `${config.watcherStaleSeconds}s`);
|
|
295
|
+
}
|
|
296
|
+
// allowedTestCommands has npm test
|
|
297
|
+
if (config) {
|
|
298
|
+
const hasNpmTest = config.allowedTestCommands.some((c) => c === "npm test" || c === "npm run test");
|
|
299
|
+
warnCheck("allowedTestCommands includes npm test", hasNpmTest, hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands");
|
|
300
|
+
}
|
|
301
|
+
if (config) {
|
|
302
|
+
check("Task timeout defaults are valid", config.defaultTaskTimeoutSeconds > 0 && config.defaultTaskTimeoutSeconds <= config.maxTaskTimeoutSeconds, `default ${config.defaultTaskTimeoutSeconds}s, max ${config.maxTaskTimeoutSeconds}s`);
|
|
303
|
+
}
|
|
304
|
+
// 13. Agent command check
|
|
305
|
+
if (config) {
|
|
306
|
+
const agents = config.agents || {};
|
|
307
|
+
for (const [name, agentCfg] of Object.entries(agents)) {
|
|
308
|
+
const cmdName = agentCfg.command;
|
|
309
|
+
const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
|
|
310
|
+
if (looksLikePath) {
|
|
311
|
+
const agentExists = existsSync(cmdName);
|
|
312
|
+
warnCheck(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Platform-appropriate lookup: 'where' on Windows, 'command -v' on Unix
|
|
316
|
+
const isWin = process.platform === "win32";
|
|
317
|
+
const lookupCmd = isWin ? `where ${cmdName}` : `command -v ${cmdName}`;
|
|
318
|
+
const fallbackCmd = isWin ? `command -v ${cmdName}` : `which ${cmdName}`;
|
|
319
|
+
const found = cmd(lookupCmd) || cmd(fallbackCmd);
|
|
320
|
+
warnCheck(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// 14. allowedTestCommands safety check
|
|
324
|
+
if (config) {
|
|
325
|
+
const testCmds = config.allowedTestCommands || [];
|
|
326
|
+
check("allowedTestCommands is non-empty", testCmds.length > 0, testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured");
|
|
327
|
+
const dangerous = ["rm -rf", "del /s", "format", "shutdown", "curl |", "wget |"];
|
|
328
|
+
for (const cmdStr of testCmds) {
|
|
329
|
+
for (const danger of dangerous) {
|
|
330
|
+
if (cmdStr.toLowerCase().includes(danger)) {
|
|
331
|
+
results.push(`[WARN] allowedTestCommands contains dangerous pattern: "${cmdStr}"`);
|
|
332
|
+
warn++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// 15. Tunnel example files check
|
|
338
|
+
const tunnelFiles = [
|
|
339
|
+
"examples/openai-tunnel/README.md",
|
|
340
|
+
"examples/openai-tunnel/tunnel-client.example.yaml",
|
|
341
|
+
"examples/openai-tunnel/chatgpt-test-prompt.md",
|
|
342
|
+
];
|
|
343
|
+
for (const tf of tunnelFiles) {
|
|
344
|
+
const full = resolve(process.cwd(), tf);
|
|
345
|
+
const exists = existsSync(full);
|
|
346
|
+
check(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing");
|
|
347
|
+
// Check for leaked secrets in example files
|
|
348
|
+
if (exists) {
|
|
349
|
+
const content = readFileSync(full, "utf-8");
|
|
350
|
+
// Only flag actual key-value assignments, not comments/mentions
|
|
351
|
+
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(
|
|
352
|
+
// Strip comment lines first
|
|
353
|
+
content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n"));
|
|
354
|
+
if (leaked) {
|
|
355
|
+
results.push(`[WARN] ${tf} may contain secrets`);
|
|
356
|
+
warn++;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
results.push(`[OK] ${tf} — no real secrets`);
|
|
360
|
+
ok++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// ══════════════════════════════════════════════════════════════════
|
|
365
|
+
// Summary
|
|
366
|
+
// ══════════════════════════════════════════════════════════════════
|
|
367
|
+
console.log(results.join("\n"));
|
|
368
|
+
console.log(`\n${"=".repeat(50)}`);
|
|
369
|
+
console.log(`OK: ${ok} WARN: ${warn} FAIL: ${fail}`);
|
|
370
|
+
console.log(`${"=".repeat(50)}`);
|
|
371
|
+
if (fail > 0) {
|
|
372
|
+
console.log("\n❌ Doctor found issues that need attention.");
|
|
373
|
+
console.log(" Fix FAIL items before using PatchWarden.");
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
else if (warn > 0) {
|
|
377
|
+
console.log("\n⚠️ Doctor found warnings — review before production use.");
|
|
378
|
+
process.exit(0);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
console.log("\n✅ All checks passed.");
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
} // end async main
|
|
385
|
+
main().catch((err) => {
|
|
386
|
+
console.error("Doctor crashed:", err);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
});
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class PatchWardenError extends Error {
|
|
2
|
+
readonly reason: string;
|
|
3
|
+
readonly suggestion: string;
|
|
4
|
+
readonly blocked: boolean;
|
|
5
|
+
readonly details: Record<string, unknown>;
|
|
6
|
+
constructor(reason: string, message: string, suggestion: string, blocked?: boolean, details?: Record<string, unknown>);
|
|
7
|
+
}
|
|
8
|
+
export declare function errorPayload(error: unknown): {
|
|
9
|
+
blocked: boolean;
|
|
10
|
+
reason: string;
|
|
11
|
+
rule_id: string;
|
|
12
|
+
error: string;
|
|
13
|
+
suggestion: string;
|
|
14
|
+
} | {
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class PatchWardenError extends Error {
|
|
2
|
+
reason;
|
|
3
|
+
suggestion;
|
|
4
|
+
blocked;
|
|
5
|
+
details;
|
|
6
|
+
constructor(reason, message, suggestion, blocked = true, details = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.suggestion = suggestion;
|
|
10
|
+
this.blocked = blocked;
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.name = "PatchWardenError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function errorPayload(error) {
|
|
16
|
+
if (error instanceof PatchWardenError) {
|
|
17
|
+
return {
|
|
18
|
+
blocked: error.blocked,
|
|
19
|
+
reason: error.reason,
|
|
20
|
+
rule_id: error.reason,
|
|
21
|
+
error: error.message,
|
|
22
|
+
suggestion: error.suggestion,
|
|
23
|
+
...error.details,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PatchWarden MCP Server — HTTP (Streamable HTTP) transport
|
|
4
|
+
*
|
|
5
|
+
* Binds to 127.0.0.1 only. Never exposes to LAN or public internet.
|
|
6
|
+
* Use with OpenAI tunnel-client or ChatGPT Connector.
|
|
7
|
+
*
|
|
8
|
+
* Each HTTP request gets its own MCP Server + transport instance
|
|
9
|
+
* to avoid "Already connected" errors from reusing a single Server.
|
|
10
|
+
*
|
|
11
|
+
* Config options (in patchwarden.config.json):
|
|
12
|
+
* httpPort: number (default 7331)
|
|
13
|
+
*
|
|
14
|
+
* Run: node dist/httpServer.js
|
|
15
|
+
* or: npm run start:http
|
|
16
|
+
*/
|
|
17
|
+
export {};
|