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