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
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { setTimeout as sleep } from "node:timers/promises";
14
+ import { CHATGPT_CORE_TOOL_NAMES } from "../dist/tools/toolCatalog.js";
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const root = resolve(__dirname, "..");
18
+ const serverPath = resolve(root, "dist", "httpServer.js");
19
+
20
+ const host = "127.0.0.1";
21
+ const port = 17331;
22
+ const mcpUrl = `http://${host}:${port}/mcp`;
23
+
24
+ const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-http-"));
25
+ const workspaceRoot = join(tempRoot, "workspace");
26
+ const configPath = join(tempRoot, "patchwarden.config.json");
27
+
28
+ let passed = 0;
29
+ let failed = 0;
30
+ let serverProcess = null;
31
+ let serverStderr = "";
32
+
33
+ function ok(name) {
34
+ console.log(` ok - ${name}`);
35
+ passed++;
36
+ }
37
+
38
+ function fail(name, error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ console.log(` not ok - ${name}: ${message}`);
41
+ failed++;
42
+ }
43
+
44
+ async function test(name, fn) {
45
+ try {
46
+ await fn();
47
+ ok(name);
48
+ } catch (error) {
49
+ fail(name, error);
50
+ }
51
+ }
52
+
53
+ function parseSseMessage(text) {
54
+ const dataLines = text
55
+ .split(/\r?\n/)
56
+ .filter((line) => line.startsWith("data:"))
57
+ .map((line) => line.slice(5).trimStart());
58
+ if (dataLines.length === 0) {
59
+ throw new Error(`No SSE data line in response: ${text.slice(0, 200)}`);
60
+ }
61
+ return JSON.parse(dataLines.join("\n"));
62
+ }
63
+
64
+ async function rpc(method, params = {}, extraHeaders = {}) {
65
+ const response = await fetch(mcpUrl, {
66
+ method: "POST",
67
+ headers: {
68
+ "content-type": "application/json",
69
+ accept: "application/json, text/event-stream",
70
+ ...extraHeaders,
71
+ },
72
+ body: JSON.stringify({
73
+ jsonrpc: "2.0",
74
+ id: Math.floor(Math.random() * 1_000_000),
75
+ method,
76
+ params,
77
+ }),
78
+ });
79
+
80
+ const text = await response.text();
81
+ if (!response.ok) {
82
+ throw new Error(`HTTP ${response.status}: ${text.slice(0, 300)}`);
83
+ }
84
+ const payload = parseSseMessage(text);
85
+ if (payload.error) {
86
+ return { isError: true, error: payload.error };
87
+ }
88
+ return payload.result;
89
+ }
90
+
91
+ function toolText(result) {
92
+ return String(result?.content?.[0]?.text || "");
93
+ }
94
+
95
+ function toolJson(result) {
96
+ return JSON.parse(toolText(result));
97
+ }
98
+
99
+ console.log("\n=== PatchWarden HTTP MCP Smoke Tests ===\n");
100
+
101
+ try {
102
+ mkdirSync(workspaceRoot, { recursive: true });
103
+ writeFileSync(join(workspaceRoot, "hello.txt"), "hello from http smoke\n", "utf-8");
104
+ writeFileSync(join(workspaceRoot, ".env"), "SECRET=blocked\n", "utf-8");
105
+ writeFileSync(
106
+ configPath,
107
+ JSON.stringify(
108
+ {
109
+ workspaceRoot,
110
+ plansDir: ".patchwarden/plans",
111
+ tasksDir: ".patchwarden/tasks",
112
+ agents: {
113
+ codex: {
114
+ command: "node",
115
+ args: ["-e", "console.log('agent placeholder')"],
116
+ },
117
+ },
118
+ allowedTestCommands: ["npm test"],
119
+ maxReadFileBytes: 200000,
120
+ httpPort: port,
121
+ },
122
+ null,
123
+ 2
124
+ ),
125
+ "utf-8"
126
+ );
127
+
128
+ console.log(`Starting HTTP MCP server on ${mcpUrl}...`);
129
+ serverProcess = spawn("node", [serverPath], {
130
+ cwd: root,
131
+ env: {
132
+ ...process.env,
133
+ PATCHWARDEN_CONFIG: configPath,
134
+ PATCHWARDEN_HTTP_PORT: String(port),
135
+ },
136
+ stdio: ["ignore", "ignore", "pipe"],
137
+ });
138
+ serverProcess.stderr.on("data", (chunk) => {
139
+ serverStderr += chunk.toString();
140
+ });
141
+
142
+ await sleep(1000);
143
+ if (serverProcess.exitCode !== null) {
144
+ throw new Error(`HTTP server exited early: ${serverStderr}`);
145
+ }
146
+
147
+ await test("initialize returns server info", async () => {
148
+ const result = await rpc("initialize", {
149
+ protocolVersion: "2024-11-05",
150
+ capabilities: {},
151
+ clientInfo: { name: "http-mcp-smoke", version: "1.0.0" },
152
+ });
153
+ if (result.serverInfo?.name !== "patchwarden") {
154
+ throw new Error(`Unexpected server info: ${JSON.stringify(result.serverInfo)}`);
155
+ }
156
+ });
157
+
158
+ await test("healthz returns structured local readiness", async () => {
159
+ const response = await fetch(`http://${host}:${port}/healthz`);
160
+ const data = await response.json();
161
+ if (!response.ok || data.mcp_server?.available !== true || data.workspace_root?.available !== true) {
162
+ throw new Error(`Unexpected health response: ${JSON.stringify(data)}`);
163
+ }
164
+ });
165
+
166
+ await test("wrong endpoint returns structured 404 guidance", async () => {
167
+ const response = await fetch(`http://${host}:${port}/wrong-path`);
168
+ const data = await response.json();
169
+ if (response.status !== 404 || data.error_code !== "mcp_endpoint_not_found" || data.expected_path !== "/mcp") {
170
+ throw new Error(`Unexpected 404 response: ${JSON.stringify(data)}`);
171
+ }
172
+ });
173
+
174
+ await test("tools/list returns expected tools", async () => {
175
+ const result = await rpc("tools/list");
176
+ const toolNames = result.tools.map((tool) => tool.name);
177
+ for (const expected of [
178
+ "save_plan",
179
+ "get_plan",
180
+ "create_task",
181
+ "get_task_status",
182
+ "get_result",
183
+ "get_diff",
184
+ "get_test_log",
185
+ "list_workspace",
186
+ "read_workspace_file",
187
+ ]) {
188
+ if (!toolNames.includes(expected)) {
189
+ throw new Error(`Missing tool ${expected}; got ${toolNames.join(", ")}`);
190
+ }
191
+ }
192
+ });
193
+
194
+ await test("list_workspace returns entries", async () => {
195
+ const result = await rpc("tools/call", {
196
+ name: "list_workspace",
197
+ arguments: {},
198
+ });
199
+ const data = toolJson(result);
200
+ if (!Array.isArray(data.entries)) {
201
+ throw new Error("Expected entries array");
202
+ }
203
+ });
204
+
205
+ await test("save_plan creates a plan", async () => {
206
+ const result = await rpc("tools/call", {
207
+ name: "save_plan",
208
+ arguments: {
209
+ title: "HTTP Test Plan",
210
+ content: "# HTTP Test\n\nHello from HTTP MCP.",
211
+ },
212
+ });
213
+ const data = toolJson(result);
214
+ if (!data.plan_id || !existsSync(data.path)) {
215
+ throw new Error(`Expected created plan, got ${JSON.stringify(data)}`);
216
+ }
217
+ });
218
+
219
+ await test("read_workspace_file reads normal files", async () => {
220
+ const result = await rpc("tools/call", {
221
+ name: "read_workspace_file",
222
+ arguments: { path: "hello.txt" },
223
+ });
224
+ const data = toolJson(result);
225
+ if (!data.content.includes("hello from http smoke")) {
226
+ throw new Error("Unexpected file content");
227
+ }
228
+ });
229
+
230
+ await test("read_workspace_file blocks .env", async () => {
231
+ const result = await rpc("tools/call", {
232
+ name: "read_workspace_file",
233
+ arguments: { path: ".env" },
234
+ });
235
+ if (!result.isError) {
236
+ throw new Error("Expected .env read to be blocked");
237
+ }
238
+ });
239
+
240
+ await test("read_workspace_file blocks path escape", async () => {
241
+ const result = await rpc("tools/call", {
242
+ name: "read_workspace_file",
243
+ arguments: { path: "../../etc/passwd" },
244
+ });
245
+ if (!result.isError) {
246
+ throw new Error("Expected path escape to be blocked");
247
+ }
248
+ });
249
+
250
+ // ═══════════════════════════════════════════════════════════
251
+ // Section: Owner Token Authentication
252
+ // ═══════════════════════════════════════════════════════════
253
+
254
+ // Kill the no-token server
255
+ serverProcess.kill("SIGKILL");
256
+ await Promise.race([
257
+ new Promise((resolve) => serverProcess.once("exit", resolve)),
258
+ sleep(2000),
259
+ ]);
260
+
261
+ const OWNER_TOKEN = "test-token-secure-abc123";
262
+
263
+ // Start server WITH owner token
264
+ console.log("\n Starting server with owner token...");
265
+ serverProcess = spawn("node", [serverPath], {
266
+ cwd: root,
267
+ env: {
268
+ ...process.env,
269
+ PATCHWARDEN_CONFIG: configPath,
270
+ PATCHWARDEN_HTTP_PORT: String(port),
271
+ PATCHWARDEN_OWNER_TOKEN: OWNER_TOKEN,
272
+ PATCHWARDEN_TOOL_PROFILE: "chatgpt_core",
273
+ },
274
+ stdio: ["ignore", "ignore", "pipe"],
275
+ });
276
+ serverProcess.stderr.on("data", (chunk) => { serverStderr += chunk.toString(); });
277
+ await sleep(1000);
278
+
279
+ await test("token: no token returns 401", async () => {
280
+ try {
281
+ await rpc("tools/list");
282
+ throw new Error("Should have been rejected");
283
+ } catch (err) {
284
+ const msg = err instanceof Error ? err.message : String(err);
285
+ if (!msg.includes("401") && !msg.includes("Unauthorized")) {
286
+ throw new Error(`Expected 401, got: ${msg.slice(0, 100)}`);
287
+ }
288
+ }
289
+ });
290
+
291
+ await test("token: wrong token returns 401", async () => {
292
+ try {
293
+ await rpc("tools/list", {}, { Authorization: "Bearer wrong-token" });
294
+ throw new Error("Should have been rejected");
295
+ } catch (err) {
296
+ const msg = err instanceof Error ? err.message : String(err);
297
+ if (!msg.includes("401")) {
298
+ throw new Error(`Expected 401, got: ${msg.slice(0, 100)}`);
299
+ }
300
+ }
301
+ });
302
+
303
+ await test("token: correct Bearer token succeeds", async () => {
304
+ const result = await rpc("tools/list", {}, { Authorization: `Bearer ${OWNER_TOKEN}` });
305
+ const names = result.tools?.map((tool) => tool.name) || [];
306
+ if (
307
+ JSON.stringify(names) !== JSON.stringify(CHATGPT_CORE_TOOL_NAMES) ||
308
+ !names.includes("wait_for_task") ||
309
+ !names.includes("get_task_summary") ||
310
+ names.includes("kill_task")
311
+ ) {
312
+ throw new Error(`Expected exact chatgpt_core tools with valid token: ${JSON.stringify(names)}`);
313
+ }
314
+ });
315
+
316
+ await test("token: correct x-patchwarden-token header succeeds", async () => {
317
+ const result = await rpc("tools/list", {}, { "x-patchwarden-token": OWNER_TOKEN });
318
+ if (!result.tools || result.tools.length === 0) {
319
+ throw new Error("Expected tools list with valid custom header token");
320
+ }
321
+ });
322
+
323
+ } catch (error) {
324
+ fail("HTTP MCP smoke setup", error);
325
+ } finally {
326
+ if (serverProcess && serverProcess.exitCode === null) {
327
+ serverProcess.kill("SIGKILL");
328
+ await Promise.race([
329
+ new Promise((resolve) => serverProcess.once("exit", resolve)),
330
+ sleep(2000),
331
+ ]);
332
+ }
333
+ try {
334
+ rmSync(tempRoot, { recursive: true, force: true });
335
+ } catch {}
336
+ }
337
+
338
+ console.log(`\n${"=".repeat(50)}`);
339
+ console.log(`${passed} passed, ${failed} failed, ${passed + failed} total`);
340
+ console.log(`${"=".repeat(50)}\n`);
341
+
342
+ if (failed > 0) {
343
+ console.error("SOME HTTP MCP TESTS FAILED");
344
+ if (serverStderr) {
345
+ console.error("\nServer stderr:");
346
+ console.error(serverStderr.slice(0, 2000));
347
+ }
348
+ process.exit(1);
349
+ }
350
+
351
+ console.log("ALL HTTP MCP TESTS PASSED\n");