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
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
15
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
16
|
+
|
|
17
|
+
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
18
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-mcp-"));
|
|
19
|
+
const workspaceRoot = join(tempRoot, "workspace");
|
|
20
|
+
const configPath = join(tempRoot, "patchwarden.config.json");
|
|
21
|
+
|
|
22
|
+
let failures = 0;
|
|
23
|
+
|
|
24
|
+
function ok(label) {
|
|
25
|
+
console.log(`ok - ${label}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fail(label, error) {
|
|
29
|
+
failures++;
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
console.error(`not ok - ${label}: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function expectToolError(client, name, args, label) {
|
|
35
|
+
const result = await client.callTool({ name, arguments: args });
|
|
36
|
+
if (!result.isError) {
|
|
37
|
+
throw new Error(`${label} should have returned an MCP tool error`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(join(tempRoot, ".keep"), "");
|
|
43
|
+
mkdirSync(workspaceRoot, { recursive: true });
|
|
44
|
+
mkdirSync(join(workspaceRoot, ".patchwarden"), { recursive: true });
|
|
45
|
+
writeFileSync(join(workspaceRoot, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
46
|
+
status: "running",
|
|
47
|
+
pid: process.pid,
|
|
48
|
+
instance_id: "mcp-smoke-watcher",
|
|
49
|
+
launcher_pid: process.pid,
|
|
50
|
+
started_at: new Date().toISOString(),
|
|
51
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
52
|
+
}), "utf-8");
|
|
53
|
+
writeFileSync(join(workspaceRoot, "hello.txt"), "hello from mcp smoke\n", "utf-8");
|
|
54
|
+
writeFileSync(join(workspaceRoot, ".env"), "SECRET=blocked\n", "utf-8");
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(workspaceRoot, "package.json"),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
name: "patchwarden-mcp-smoke-fixture",
|
|
59
|
+
private: true,
|
|
60
|
+
scripts: { test: "node -e \"console.log('test ok')\"" },
|
|
61
|
+
}, null, 2),
|
|
62
|
+
"utf-8"
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
writeFileSync(
|
|
66
|
+
configPath,
|
|
67
|
+
JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
workspaceRoot,
|
|
70
|
+
plansDir: ".patchwarden/plans",
|
|
71
|
+
tasksDir: ".patchwarden/tasks",
|
|
72
|
+
agents: {
|
|
73
|
+
codex: {
|
|
74
|
+
command: "node",
|
|
75
|
+
args: ["-e", "console.log('agent placeholder')"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
allowedTestCommands: ["npm test"],
|
|
79
|
+
maxReadFileBytes: 200000,
|
|
80
|
+
},
|
|
81
|
+
null,
|
|
82
|
+
2
|
|
83
|
+
),
|
|
84
|
+
"utf-8"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const transport = new StdioClientTransport({
|
|
88
|
+
command: "node",
|
|
89
|
+
args: ["dist/index.js"],
|
|
90
|
+
cwd: root,
|
|
91
|
+
env: { PATCHWARDEN_CONFIG: configPath },
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
});
|
|
94
|
+
const client = new Client(
|
|
95
|
+
{ name: "patchwarden-smoke", version: "0.1.0" },
|
|
96
|
+
{ capabilities: {} }
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await client.connect(transport);
|
|
100
|
+
|
|
101
|
+
const tools = await client.listTools();
|
|
102
|
+
const names = tools.tools.map((tool) => tool.name).sort();
|
|
103
|
+
const expected = [
|
|
104
|
+
"audit_task",
|
|
105
|
+
"cancel_task",
|
|
106
|
+
"create_task",
|
|
107
|
+
"get_diff",
|
|
108
|
+
"get_plan",
|
|
109
|
+
"get_result",
|
|
110
|
+
"get_result_json",
|
|
111
|
+
"get_task_log_tail",
|
|
112
|
+
"get_task_progress",
|
|
113
|
+
"get_task_status",
|
|
114
|
+
"get_task_stdout_tail",
|
|
115
|
+
"get_task_summary",
|
|
116
|
+
"get_test_log",
|
|
117
|
+
"health_check",
|
|
118
|
+
"kill_task",
|
|
119
|
+
"list_agents",
|
|
120
|
+
"list_tasks",
|
|
121
|
+
"list_workspace",
|
|
122
|
+
"read_workspace_file",
|
|
123
|
+
"retry_task",
|
|
124
|
+
"save_plan",
|
|
125
|
+
"wait_for_task",
|
|
126
|
+
];
|
|
127
|
+
if (JSON.stringify(names) !== JSON.stringify(expected)) {
|
|
128
|
+
throw new Error(`unexpected tools: ${names.join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
ok("MCP handshake lists all tools");
|
|
131
|
+
|
|
132
|
+
// Layer 2: tools/list response must carry _meta with manifest hash for catalog sync
|
|
133
|
+
if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
|
|
134
|
+
throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
|
|
135
|
+
}
|
|
136
|
+
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 22) {
|
|
137
|
+
throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
|
|
138
|
+
}
|
|
139
|
+
if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
|
|
140
|
+
throw new Error(`tools/list _meta missing schema_epoch/server_version: ${JSON.stringify(tools._meta)}`);
|
|
141
|
+
}
|
|
142
|
+
ok("tools/list _meta carries manifest hash, schema epoch, and profile");
|
|
143
|
+
|
|
144
|
+
const parseToolJson = async (name, args) => {
|
|
145
|
+
const result = await client.callTool({ name, arguments: args });
|
|
146
|
+
if (result.isError) {
|
|
147
|
+
throw new Error(result.content?.[0]?.text || `${name} failed`);
|
|
148
|
+
}
|
|
149
|
+
return JSON.parse(result.content?.[0]?.text || "{}");
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const agents = await parseToolJson("list_agents", {});
|
|
153
|
+
if (agents.total !== 1 || agents.agents?.[0]?.name !== "codex" || !agents.agents[0].available) {
|
|
154
|
+
throw new Error(`list_agents mismatch: ${JSON.stringify(agents)}`);
|
|
155
|
+
}
|
|
156
|
+
const health = await parseToolJson("health_check", {});
|
|
157
|
+
if (!health.mcp_server?.available) {
|
|
158
|
+
throw new Error(`health_check did not report MCP server availability: ${JSON.stringify(health)}`);
|
|
159
|
+
}
|
|
160
|
+
const diagnostic = await parseToolJson("health_check", { detail: "self_diagnostic" });
|
|
161
|
+
if (
|
|
162
|
+
diagnostic.connector_visibility?.status !== "not_observable_server_side" ||
|
|
163
|
+
diagnostic.self_diagnostic?.mode !== "self_diagnostic" ||
|
|
164
|
+
!Array.isArray(diagnostic.self_diagnostic?.configured_agents)
|
|
165
|
+
) {
|
|
166
|
+
throw new Error(`self diagnostic health evidence is incomplete: ${JSON.stringify(diagnostic)}`);
|
|
167
|
+
}
|
|
168
|
+
ok("list_agents and health_check report runtime readiness");
|
|
169
|
+
|
|
170
|
+
const plan = await parseToolJson("save_plan", {
|
|
171
|
+
title: "MCP smoke",
|
|
172
|
+
content: "# Smoke\n\nVerify MCP tool calls.",
|
|
173
|
+
});
|
|
174
|
+
const readPlan = await parseToolJson("get_plan", { plan_id: plan.plan_id });
|
|
175
|
+
if (!readPlan.content.includes("Verify MCP tool calls.")) {
|
|
176
|
+
throw new Error("saved plan content was not readable");
|
|
177
|
+
}
|
|
178
|
+
ok("save_plan and get_plan work");
|
|
179
|
+
|
|
180
|
+
const unsafePlan = await client.callTool({
|
|
181
|
+
name: "save_plan",
|
|
182
|
+
arguments: { title: "Unsafe", content: "Read the .env access token and export credentials." },
|
|
183
|
+
});
|
|
184
|
+
const unsafePayload = JSON.parse(unsafePlan.content?.[0]?.text || "{}");
|
|
185
|
+
if (!unsafePlan.isError || unsafePayload.operation !== "save_plan" || !unsafePayload.rule_id || !unsafePayload.matched_category) {
|
|
186
|
+
throw new Error(`save_plan block is not structured: ${JSON.stringify(unsafePayload)}`);
|
|
187
|
+
}
|
|
188
|
+
ok("save_plan blocks explicit credential access with structured evidence");
|
|
189
|
+
|
|
190
|
+
const missingRepo = await client.callTool({
|
|
191
|
+
name: "create_task",
|
|
192
|
+
arguments: { plan_id: plan.plan_id, agent: "codex" },
|
|
193
|
+
});
|
|
194
|
+
const missingRepoPayload = JSON.parse(missingRepo.content?.[0]?.text || "{}");
|
|
195
|
+
if (!missingRepo.isError || missingRepoPayload.reason !== "repo_path_required") {
|
|
196
|
+
throw new Error(`missing repo_path should be a structured error: ${JSON.stringify(missingRepoPayload)}`);
|
|
197
|
+
}
|
|
198
|
+
ok("create_task requires an explicit repo_path");
|
|
199
|
+
|
|
200
|
+
const task = await parseToolJson("create_task", {
|
|
201
|
+
plan_id: plan.plan_id,
|
|
202
|
+
agent: "codex",
|
|
203
|
+
repo_path: ".",
|
|
204
|
+
test_command: "npm test",
|
|
205
|
+
});
|
|
206
|
+
if (
|
|
207
|
+
task.server_version !== "0.4.0" ||
|
|
208
|
+
!/^[a-f0-9]{64}$/.test(task.tool_manifest_sha256 || "") ||
|
|
209
|
+
task.next_tool_call?.name !== "wait_for_task" ||
|
|
210
|
+
task.next_tool_call?.arguments?.timeout_seconds !== 25
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(`create_task handoff metadata mismatch: ${JSON.stringify(task)}`);
|
|
213
|
+
}
|
|
214
|
+
const status = await parseToolJson("get_task_status", { task_id: task.task_id });
|
|
215
|
+
if (status.status !== "pending") {
|
|
216
|
+
throw new Error(`expected pending task, got ${status.status}`);
|
|
217
|
+
}
|
|
218
|
+
if (status.phase !== "queued" || !status.last_heartbeat_at || status.timeout_seconds !== 900) {
|
|
219
|
+
throw new Error(`expected queued phase, heartbeat, and default timeout; got ${JSON.stringify(status)}`);
|
|
220
|
+
}
|
|
221
|
+
const progress = await parseToolJson("get_task_progress", { task_id: task.task_id });
|
|
222
|
+
if (!progress.content.includes("Waiting for watcher")) {
|
|
223
|
+
throw new Error("get_task_progress did not return queued progress");
|
|
224
|
+
}
|
|
225
|
+
ok("create_task and get_task_status work");
|
|
226
|
+
|
|
227
|
+
const inlineTask = await parseToolJson("create_task", {
|
|
228
|
+
inline_plan: "Inspect the repository and report findings without changing files.",
|
|
229
|
+
plan_title: "Inline MCP smoke",
|
|
230
|
+
agent: "codex",
|
|
231
|
+
repo_path: ".",
|
|
232
|
+
});
|
|
233
|
+
if (inlineTask.plan_source !== "inline" || !inlineTask.plan_id) {
|
|
234
|
+
throw new Error(`inline_plan was not persisted: ${JSON.stringify(inlineTask)}`);
|
|
235
|
+
}
|
|
236
|
+
const templateTask = await parseToolJson("create_task", {
|
|
237
|
+
template: "inspect_only",
|
|
238
|
+
goal: "Inspect package metadata",
|
|
239
|
+
agent: "codex",
|
|
240
|
+
repo_path: ".",
|
|
241
|
+
});
|
|
242
|
+
if (templateTask.plan_source !== "template" || templateTask.change_policy !== "no_changes") {
|
|
243
|
+
throw new Error(`guarded template metadata mismatch: ${JSON.stringify(templateTask)}`);
|
|
244
|
+
}
|
|
245
|
+
ok("create_task accepts inline plans and guarded templates");
|
|
246
|
+
|
|
247
|
+
const blockedAgent = await client.callTool({
|
|
248
|
+
name: "create_task",
|
|
249
|
+
arguments: { plan_id: plan.plan_id, agent: "missing-agent", repo_path: "." },
|
|
250
|
+
});
|
|
251
|
+
const blockedPayload = JSON.parse(blockedAgent.content?.[0]?.text || "{}");
|
|
252
|
+
if (!blockedAgent.isError || blockedPayload.reason !== "agent_not_configured" || !blockedPayload.suggestion) {
|
|
253
|
+
throw new Error(`expected structured agent block, got ${JSON.stringify(blockedPayload)}`);
|
|
254
|
+
}
|
|
255
|
+
ok("security blocks return structured reason and suggestion");
|
|
256
|
+
|
|
257
|
+
const file = await parseToolJson("read_workspace_file", { path: "hello.txt" });
|
|
258
|
+
if (!file.content.includes("hello from mcp smoke")) {
|
|
259
|
+
throw new Error("workspace file content mismatch");
|
|
260
|
+
}
|
|
261
|
+
ok("read_workspace_file reads normal files");
|
|
262
|
+
|
|
263
|
+
await expectToolError(client, "read_workspace_file", { path: ".env" }, "sensitive file");
|
|
264
|
+
const sensitive = await client.callTool({ name: "read_workspace_file", arguments: { path: ".env" } });
|
|
265
|
+
const sensitivePayload = JSON.parse(sensitive.content?.[0]?.text || "{}");
|
|
266
|
+
if (sensitivePayload.rule_id !== "sensitive_path_blocked" || sensitivePayload.operation !== "read") {
|
|
267
|
+
throw new Error(`sensitive block is not structured: ${JSON.stringify(sensitivePayload)}`);
|
|
268
|
+
}
|
|
269
|
+
await expectToolError(
|
|
270
|
+
client,
|
|
271
|
+
"read_workspace_file",
|
|
272
|
+
{ path: "../outside.txt" },
|
|
273
|
+
"path escape"
|
|
274
|
+
);
|
|
275
|
+
ok("sensitive file and path escape checks reject access");
|
|
276
|
+
|
|
277
|
+
const runner = spawnSync("node", ["dist/runner/cli.js", task.task_id], {
|
|
278
|
+
cwd: root,
|
|
279
|
+
env: { ...process.env, PATCHWARDEN_CONFIG: configPath },
|
|
280
|
+
encoding: "utf-8",
|
|
281
|
+
timeout: 30000,
|
|
282
|
+
});
|
|
283
|
+
if (runner.status !== 0) {
|
|
284
|
+
throw new Error(`runner exited ${runner.status}: ${runner.stderr}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const statusPath = join(task.path, "status.json");
|
|
288
|
+
const statusAfter = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
289
|
+
if (statusAfter.status !== "done") {
|
|
290
|
+
throw new Error(`runner status should be done, got ${statusAfter.status}`);
|
|
291
|
+
}
|
|
292
|
+
for (const fileName of ["result.md", "result.json", "diff.patch", "git.diff", "file-stats.json", "test.log", "verify.json", "verify.log"]) {
|
|
293
|
+
if (!existsSync(join(task.path, fileName))) {
|
|
294
|
+
throw new Error(`runner did not create ${fileName}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const summary = await parseToolJson("get_task_summary", { task_id: task.task_id });
|
|
298
|
+
if (!summary.terminal || summary.acceptance_status !== "ready_for_review") {
|
|
299
|
+
throw new Error(`unexpected terminal summary: ${JSON.stringify(summary)}`);
|
|
300
|
+
}
|
|
301
|
+
const waited = await parseToolJson("wait_for_task", { task_id: task.task_id, timeout_seconds: 1 });
|
|
302
|
+
if (!waited.terminal || waited.continuation_required || waited.next_tool_call?.name !== "audit_task") {
|
|
303
|
+
throw new Error(`wait_for_task did not return terminal acceptance: ${JSON.stringify(waited)}`);
|
|
304
|
+
}
|
|
305
|
+
const legacyWaited = await parseToolJson("wait_for_task", { task_id: task.task_id, wait_seconds: 1 });
|
|
306
|
+
if (!legacyWaited.terminal) throw new Error("legacy wait_seconds alias stopped working");
|
|
307
|
+
const conflictingWait = await client.callTool({
|
|
308
|
+
name: "wait_for_task",
|
|
309
|
+
arguments: { task_id: task.task_id, timeout_seconds: 1, wait_seconds: 2 },
|
|
310
|
+
});
|
|
311
|
+
if (!conflictingWait.isError || !conflictingWait.content?.[0]?.text?.includes("must match")) {
|
|
312
|
+
throw new Error(`conflicting wait aliases were not rejected: ${JSON.stringify(conflictingWait)}`);
|
|
313
|
+
}
|
|
314
|
+
writeFileSync(join(task.path, "result.md"), "npm test passed\naccess_token=real-secret-value-123456\n", "utf-8");
|
|
315
|
+
const redactedResult = await parseToolJson("get_result", { task_id: task.task_id });
|
|
316
|
+
if (!redactedResult.redacted || redactedResult.content.includes("real-secret-value-123456")) {
|
|
317
|
+
throw new Error(`get_result did not redact secret-like content: ${JSON.stringify(redactedResult)}`);
|
|
318
|
+
}
|
|
319
|
+
const relativeResultPath = `.patchwarden/tasks/${task.task_id}/result.md`;
|
|
320
|
+
const redactedWorkspaceRead = await parseToolJson("read_workspace_file", { path: relativeResultPath });
|
|
321
|
+
if (!redactedWorkspaceRead.redacted || redactedWorkspaceRead.content.includes("real-secret-value-123456")) {
|
|
322
|
+
throw new Error(`read_workspace_file did not redact task artifact: ${JSON.stringify(redactedWorkspaceRead)}`);
|
|
323
|
+
}
|
|
324
|
+
await client.close();
|
|
325
|
+
ok("runner executes a task and writes result files");
|
|
326
|
+
} catch (error) {
|
|
327
|
+
fail("MCP smoke test", error);
|
|
328
|
+
} finally {
|
|
329
|
+
try {
|
|
330
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (failures > 0) {
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { join, relative, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
15
|
+
const root = resolve(scriptDir, "..");
|
|
16
|
+
const releaseDir = resolve(root, "release");
|
|
17
|
+
const archivePath = resolve(root, "patchwarden-release.tar.gz");
|
|
18
|
+
|
|
19
|
+
const include = [
|
|
20
|
+
"dist",
|
|
21
|
+
"docs",
|
|
22
|
+
"examples",
|
|
23
|
+
"scripts",
|
|
24
|
+
"src",
|
|
25
|
+
"Start-PatchWarden-Tunnel.cmd",
|
|
26
|
+
"Restart-PatchWarden.cmd",
|
|
27
|
+
"Check-PatchWarden-Health.cmd",
|
|
28
|
+
"Reset-PatchWarden-Tunnel-Key.cmd",
|
|
29
|
+
".gitignore",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"README.md",
|
|
32
|
+
"package-lock.json",
|
|
33
|
+
"package.json",
|
|
34
|
+
"tsconfig.json",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const forbidden = [
|
|
38
|
+
/(^|[\\/])node_modules([\\/]|$)/,
|
|
39
|
+
/(^|[\\/])\.patchwarden([\\/]|$)/,
|
|
40
|
+
/(^|[\\/])\.safe-bifrost([\\/]|$)/,
|
|
41
|
+
/(^|[\\/])patchwarden\.config\.json$/,
|
|
42
|
+
/(^|[\\/])safe-bifrost\.config\.json$/,
|
|
43
|
+
/\.local\.(cmd|ps1)$/i,
|
|
44
|
+
/\.dpapi$/i,
|
|
45
|
+
/(^|[\\/])\.env$/,
|
|
46
|
+
/\.log$/,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
console.log("[pack-clean] Preparing clean release directory...");
|
|
50
|
+
rmSync(releaseDir, { recursive: true, force: true });
|
|
51
|
+
rmSync(archivePath, { force: true });
|
|
52
|
+
mkdirSync(releaseDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
for (const item of include) {
|
|
55
|
+
const source = resolve(root, item);
|
|
56
|
+
const target = resolve(releaseDir, item);
|
|
57
|
+
if (!existsSync(source)) {
|
|
58
|
+
console.log(` skip missing: ${item}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
cpSync(source, target, {
|
|
62
|
+
recursive: true,
|
|
63
|
+
filter(sourcePath) {
|
|
64
|
+
const rel = toPosix(relative(root, sourcePath));
|
|
65
|
+
return !isForbidden(rel);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
console.log(` copied: ${item}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const releaseFiles = listFiles(releaseDir);
|
|
72
|
+
const badReleaseEntries = releaseFiles.filter((file) =>
|
|
73
|
+
isForbidden(toPosix(relative(releaseDir, file)))
|
|
74
|
+
);
|
|
75
|
+
if (badReleaseEntries.length > 0) {
|
|
76
|
+
console.error("[pack-clean] Forbidden files in release directory:");
|
|
77
|
+
for (const file of badReleaseEntries) {
|
|
78
|
+
console.error(` ${relative(releaseDir, file)}`);
|
|
79
|
+
}
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log("[pack-clean] Creating tar.gz archive...");
|
|
84
|
+
|
|
85
|
+
// Convert Windows paths to WSL/Linux paths when tar is a Unix binary
|
|
86
|
+
function toTarPath(p) {
|
|
87
|
+
if (process.platform === "win32") {
|
|
88
|
+
return p;
|
|
89
|
+
}
|
|
90
|
+
return p.replace(/^([A-Za-z]):/, (_, d) => `/${d.toLowerCase()}`).replace(/\\/g, "/");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const tarArchivePath = toTarPath(archivePath);
|
|
94
|
+
const tarReleaseDir = toTarPath(releaseDir);
|
|
95
|
+
|
|
96
|
+
execFileSync("tar", ["-czf", tarArchivePath, "-C", tarReleaseDir, "."], {
|
|
97
|
+
stdio: "inherit",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const archiveEntries = execFileSync("tar", ["-tzf", tarArchivePath], {
|
|
101
|
+
encoding: "utf-8",
|
|
102
|
+
})
|
|
103
|
+
.split(/\r?\n/)
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.map((entry) => toPosix(entry.replace(/^\.\//, "")));
|
|
106
|
+
|
|
107
|
+
const badArchiveEntries = archiveEntries.filter(isForbidden);
|
|
108
|
+
if (badArchiveEntries.length > 0) {
|
|
109
|
+
console.error("[pack-clean] Forbidden files in archive:");
|
|
110
|
+
for (const entry of badArchiveEntries) {
|
|
111
|
+
console.error(` ${entry}`);
|
|
112
|
+
}
|
|
113
|
+
rmSync(archivePath, { force: true });
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sizeKb = (statSync(archivePath).size / 1024).toFixed(1);
|
|
118
|
+
console.log(`[pack-clean] Release directory: ${releaseFiles.length} files`);
|
|
119
|
+
console.log(`[pack-clean] Archive: ${archivePath} (${sizeKb} KB)`);
|
|
120
|
+
console.log("[pack-clean] OK");
|
|
121
|
+
|
|
122
|
+
function listFiles(dir) {
|
|
123
|
+
const entries = [];
|
|
124
|
+
for (const name of readdirSync(dir)) {
|
|
125
|
+
const fullPath = join(dir, name);
|
|
126
|
+
const stat = statSync(fullPath);
|
|
127
|
+
if (stat.isDirectory()) {
|
|
128
|
+
entries.push(...listFiles(fullPath));
|
|
129
|
+
} else {
|
|
130
|
+
entries.push(fullPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return entries;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isForbidden(value) {
|
|
137
|
+
return forbidden.some((pattern) => pattern.test(value));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function toPosix(value) {
|
|
141
|
+
return value.replace(/\\/g, "/");
|
|
142
|
+
}
|