patchwarden 0.4.0 → 0.6.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 (139) hide show
  1. package/PatchWarden.cmd +51 -0
  2. package/README.en.md +1022 -0
  3. package/README.md +897 -358
  4. package/dist/assessments/agentAssessor.d.ts +15 -0
  5. package/dist/assessments/agentAssessor.js +293 -0
  6. package/dist/assessments/assessmentStore.d.ts +133 -0
  7. package/dist/assessments/assessmentStore.js +238 -0
  8. package/dist/assessments/confirmCli.d.ts +9 -0
  9. package/dist/assessments/confirmCli.js +26 -0
  10. package/dist/config.d.ts +22 -1
  11. package/dist/config.js +156 -3
  12. package/dist/direct/directAudit.d.ts +23 -0
  13. package/dist/direct/directAudit.js +309 -0
  14. package/dist/direct/directGuards.d.ts +20 -0
  15. package/dist/direct/directGuards.js +137 -0
  16. package/dist/direct/directPatch.d.ts +17 -0
  17. package/dist/direct/directPatch.js +113 -0
  18. package/dist/direct/directSessionStore.d.ts +63 -0
  19. package/dist/direct/directSessionStore.js +192 -0
  20. package/dist/direct/directVerification.d.ts +12 -0
  21. package/dist/direct/directVerification.js +96 -0
  22. package/dist/doctor.js +73 -5
  23. package/dist/runner/agentInvocation.d.ts +23 -0
  24. package/dist/runner/agentInvocation.js +106 -0
  25. package/dist/runner/changeCapture.d.ts +29 -0
  26. package/dist/runner/changeCapture.js +107 -5
  27. package/dist/runner/runTask.js +70 -44
  28. package/dist/runner/simpleProcess.d.ts +21 -0
  29. package/dist/runner/simpleProcess.js +206 -0
  30. package/dist/runner/watch.js +17 -2
  31. package/dist/security/commandGuard.d.ts +2 -1
  32. package/dist/security/commandGuard.js +25 -3
  33. package/dist/security/riskEngine.d.ts +27 -0
  34. package/dist/security/riskEngine.js +118 -0
  35. package/dist/security/runtimeGuard.d.ts +6 -0
  36. package/dist/security/runtimeGuard.js +28 -0
  37. package/dist/smoke-test.js +1353 -7
  38. package/dist/tools/applyPatch.d.ts +16 -0
  39. package/dist/tools/applyPatch.js +41 -0
  40. package/dist/tools/auditSession.d.ts +5 -0
  41. package/dist/tools/auditSession.js +12 -0
  42. package/dist/tools/auditTask.d.ts +7 -0
  43. package/dist/tools/auditTask.js +94 -2
  44. package/dist/tools/createDirectSession.d.ts +14 -0
  45. package/dist/tools/createDirectSession.js +54 -0
  46. package/dist/tools/createTask.d.ts +48 -1
  47. package/dist/tools/createTask.js +298 -47
  48. package/dist/tools/finalizeDirectSession.d.ts +19 -0
  49. package/dist/tools/finalizeDirectSession.js +84 -0
  50. package/dist/tools/getTaskSummary.d.ts +47 -0
  51. package/dist/tools/getTaskSummary.js +64 -1
  52. package/dist/tools/healthCheck.d.ts +22 -12
  53. package/dist/tools/healthCheck.js +23 -6
  54. package/dist/tools/readWorkspaceFile.d.ts +7 -1
  55. package/dist/tools/readWorkspaceFile.js +48 -2
  56. package/dist/tools/registry.js +217 -19
  57. package/dist/tools/runVerification.d.ts +16 -0
  58. package/dist/tools/runVerification.js +32 -0
  59. package/dist/tools/savePlan.d.ts +1 -0
  60. package/dist/tools/savePlan.js +38 -7
  61. package/dist/tools/searchWorkspace.d.ts +19 -0
  62. package/dist/tools/searchWorkspace.js +205 -0
  63. package/dist/tools/taskTemplates.js +2 -1
  64. package/dist/tools/toolCatalog.d.ts +3 -2
  65. package/dist/tools/toolCatalog.js +32 -5
  66. package/dist/tools/waitForTask.d.ts +2 -2
  67. package/dist/tools/waitForTask.js +1 -1
  68. package/dist/version.d.ts +2 -2
  69. package/dist/version.js +2 -2
  70. package/docs/chatgpt-usage.md +101 -0
  71. package/docs/release-checklist.md +14 -0
  72. package/docs/release-v0.6.0.md +71 -0
  73. package/examples/config.example.json +5 -0
  74. package/examples/openai-tunnel/README.md +11 -4
  75. package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
  76. package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
  77. package/package.json +12 -8
  78. package/scripts/brand-check.js +58 -12
  79. package/scripts/control-smoke.js +206 -0
  80. package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
  81. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
  82. package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
  83. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
  84. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
  85. package/scripts/lifecycle-smoke.js +66 -6
  86. package/scripts/manage-patchwarden.ps1 +639 -0
  87. package/scripts/mcp-manifest-check.js +100 -58
  88. package/scripts/mcp-smoke.js +157 -2
  89. package/scripts/pack-clean.js +3 -4
  90. package/scripts/package-manifest-check.js +78 -0
  91. package/scripts/patchwarden-mcp-direct.cmd +7 -0
  92. package/scripts/patchwarden-mcp-stdio.cmd +1 -1
  93. package/scripts/restart-patchwarden.ps1 +5 -4
  94. package/scripts/start-patchwarden-tunnel.ps1 +261 -30
  95. package/scripts/tunnel-supervisor-smoke.js +36 -4
  96. package/scripts/watcher-supervisor-smoke.js +10 -6
  97. package/src/assessments/agentAssessor.ts +324 -0
  98. package/src/assessments/assessmentStore.ts +426 -0
  99. package/src/assessments/confirmCli.ts +29 -0
  100. package/src/config.ts +178 -4
  101. package/src/direct/directAudit.ts +400 -0
  102. package/src/direct/directGuards.ts +279 -0
  103. package/src/direct/directPatch.ts +258 -0
  104. package/src/direct/directSessionStore.ts +345 -0
  105. package/src/direct/directVerification.ts +138 -0
  106. package/src/doctor.ts +103 -7
  107. package/src/runner/agentInvocation.ts +125 -0
  108. package/src/runner/changeCapture.ts +140 -5
  109. package/src/runner/runTask.ts +61 -43
  110. package/src/runner/simpleProcess.ts +223 -0
  111. package/src/runner/watch.ts +18 -2
  112. package/src/security/commandGuard.ts +46 -4
  113. package/src/security/riskEngine.ts +160 -0
  114. package/src/security/runtimeGuard.ts +41 -0
  115. package/src/smoke-test.ts +1291 -4
  116. package/src/tools/applyPatch.ts +86 -0
  117. package/src/tools/auditSession.ts +28 -0
  118. package/src/tools/auditTask.ts +100 -2
  119. package/src/tools/createDirectSession.ts +113 -0
  120. package/src/tools/createTask.ts +405 -55
  121. package/src/tools/finalizeDirectSession.ts +144 -0
  122. package/src/tools/getTaskSummary.ts +111 -1
  123. package/src/tools/healthCheck.ts +23 -6
  124. package/src/tools/readWorkspaceFile.ts +85 -2
  125. package/src/tools/registry.ts +242 -19
  126. package/src/tools/runVerification.ts +58 -0
  127. package/src/tools/savePlan.ts +57 -7
  128. package/src/tools/searchWorkspace.ts +275 -0
  129. package/src/tools/taskTemplates.ts +2 -1
  130. package/src/tools/toolCatalog.ts +35 -6
  131. package/src/tools/waitForTask.ts +3 -3
  132. package/src/version.ts +2 -2
  133. package/tsconfig.json +18 -17
  134. package/Check-PatchWarden-Health.cmd +0 -6
  135. package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
  136. package/Restart-PatchWarden.cmd +0 -19
  137. package/Start-PatchWarden-Tunnel.cmd +0 -7
  138. package/docs/release-v0.3.0.md +0 -43
  139. package/docs/release-v0.4.0.md +0 -74
@@ -0,0 +1,324 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { resolve, relative, isAbsolute } from "node:path";
3
+ import { PatchWardenConfig } from "../config.js";
4
+ import { buildAgentInvocation, buildAssessmentPrompt } from "../runner/agentInvocation.js";
5
+ import { runSimpleProcessSync } from "../runner/simpleProcess.js";
6
+ import { captureRepoSnapshot, compareSnapshots, type RepoSnapshot, type ChangedFile } from "../runner/changeCapture.js";
7
+ import type { AgentAssessmentOutput, AgentAssessmentSummary } from "./assessmentStore.js";
8
+
9
+ const ASSESSMENT_MARKER = "===ASSESSMENT_JSON===";
10
+
11
+ export interface AgentAssessorInput {
12
+ assessmentId: string;
13
+ assessmentDir: string;
14
+ agentName: string;
15
+ repoPath: string;
16
+ workspaceRoot: string;
17
+ goal: string;
18
+ planContent: string;
19
+ timeoutSeconds: number;
20
+ maxOutputBytes: number;
21
+ config: PatchWardenConfig;
22
+ }
23
+
24
+ export function runAgentAssessment(input: AgentAssessorInput): AgentAssessmentSummary {
25
+ const logPaths: AgentAssessmentSummary["log_paths"] = {
26
+ stdout: resolve(input.assessmentDir, "agent-assessment-stdout.log"),
27
+ stderr: resolve(input.assessmentDir, "agent-assessment-stderr.log"),
28
+ assessment: resolve(input.assessmentDir, "agent-assessment.json"),
29
+ };
30
+
31
+ const emptySummary: AgentAssessmentSummary = {
32
+ attempted: false,
33
+ status: "not_run",
34
+ output: null,
35
+ merged_risk: "low",
36
+ merged_decision: "allow",
37
+ merged_reason_codes: [],
38
+ timed_out: false,
39
+ exit_code: null,
40
+ read_only_violation: false,
41
+ violation_files: [],
42
+ stdout_truncated: false,
43
+ stderr_truncated: false,
44
+ log_paths: logPaths,
45
+ };
46
+
47
+ // ── 1. Build prompt and write to file for {prompt_file} support ──
48
+ const assessmentPrompt = buildAssessmentPrompt(input.goal, input.planContent, input.repoPath);
49
+ const promptFilePath = resolve(input.assessmentDir, "agent-assessment-prompt.md");
50
+ writeFileSync(promptFilePath, assessmentPrompt, "utf-8");
51
+ logPaths.prompt = promptFilePath;
52
+
53
+ // ── 2. Before snapshot (repo-scoped) ──
54
+ let repoBefore: RepoSnapshot;
55
+ try {
56
+ repoBefore = captureRepoSnapshot(input.repoPath);
57
+ } catch {
58
+ // If we can't capture before snapshot, conservatively skip agent assessment
59
+ emptySummary.status = "spawn_failed";
60
+ emptySummary.merged_risk = "medium";
61
+ emptySummary.merged_decision = "needs_confirm";
62
+ emptySummary.merged_reason_codes = ["agent_assessment_snapshot_failed"];
63
+ writeSummary(logPaths.assessment, emptySummary);
64
+ return emptySummary;
65
+ }
66
+
67
+ // ── 3. Build agent invocation ──
68
+ let invocation;
69
+ try {
70
+ invocation = buildAgentInvocation(input.agentName, input.repoPath, assessmentPrompt, input.config, promptFilePath);
71
+ } catch (error) {
72
+ emptySummary.status = "spawn_failed";
73
+ emptySummary.merged_risk = "medium";
74
+ emptySummary.merged_decision = "needs_confirm";
75
+ emptySummary.merged_reason_codes = ["agent_assessment_invocation_failed"];
76
+ writeSummary(logPaths.assessment, emptySummary);
77
+ return emptySummary;
78
+ }
79
+
80
+ emptySummary.attempted = true;
81
+
82
+ // ── 4. Run agent ──
83
+ const result = runSimpleProcessSync({
84
+ command: invocation.command,
85
+ args: invocation.args,
86
+ cwd: input.repoPath,
87
+ timeoutMs: input.timeoutSeconds * 1000,
88
+ maxStdoutBytes: input.maxOutputBytes,
89
+ maxStderrBytes: Math.max(16384, Math.floor(input.maxOutputBytes / 4)),
90
+ stdoutPath: logPaths.stdout,
91
+ stderrPath: logPaths.stderr,
92
+ });
93
+
94
+ // ── 5. After snapshot + read-only violation check ──
95
+ let readOnlyViolation = false;
96
+ let violationFiles: string[] = [];
97
+ try {
98
+ const repoAfter = captureRepoSnapshot(input.repoPath);
99
+ const changes = compareSnapshots(repoBefore, repoAfter);
100
+ if (changes.length > 0) {
101
+ readOnlyViolation = true;
102
+ violationFiles = changes.map((c) => c.path);
103
+ }
104
+ } catch {
105
+ // 修复 #4: after snapshot 捕获失败 → 保守变为 high / blocked
106
+ readOnlyViolation = true;
107
+ violationFiles = ["(after snapshot capture failed)"];
108
+ }
109
+
110
+ if (readOnlyViolation) {
111
+ const summary: AgentAssessmentSummary = {
112
+ attempted: true,
113
+ status: "read_only_violation",
114
+ output: null,
115
+ merged_risk: "high",
116
+ merged_decision: "blocked",
117
+ merged_reason_codes: ["agent_read_only_violation"],
118
+ timed_out: false,
119
+ exit_code: result.exitCode,
120
+ read_only_violation: true,
121
+ violation_files: violationFiles,
122
+ stdout_truncated: result.stdoutTruncated,
123
+ stderr_truncated: result.stderrTruncated,
124
+ log_paths: { ...logPaths, violation: resolve(input.assessmentDir, "agent-assessment-violation.json") },
125
+ };
126
+ writeFileSync(summary.log_paths.violation!, JSON.stringify({
127
+ violation: "read_only_violation",
128
+ files: violationFiles,
129
+ exit_code: result.exitCode,
130
+ }, null, 2), "utf-8");
131
+ writeSummary(logPaths.assessment, summary);
132
+ return summary;
133
+ }
134
+
135
+ // ── 6. Handle spawn failure ──
136
+ if (result.spawnError) {
137
+ const summary: AgentAssessmentSummary = {
138
+ ...emptySummary,
139
+ attempted: true,
140
+ status: "spawn_failed",
141
+ merged_risk: "medium",
142
+ merged_decision: "needs_confirm",
143
+ merged_reason_codes: ["agent_assessment_spawn_failed"],
144
+ exit_code: result.exitCode,
145
+ stdout_truncated: result.stdoutTruncated,
146
+ stderr_truncated: result.stderrTruncated,
147
+ };
148
+ writeSummary(logPaths.assessment, summary);
149
+ return summary;
150
+ }
151
+
152
+ // ── 7. Handle timeout ──
153
+ if (result.timedOut) {
154
+ const summary: AgentAssessmentSummary = {
155
+ ...emptySummary,
156
+ attempted: true,
157
+ status: "timed_out",
158
+ merged_risk: "medium",
159
+ merged_decision: "needs_confirm",
160
+ merged_reason_codes: ["agent_assessment_timed_out"],
161
+ timed_out: true,
162
+ exit_code: result.exitCode,
163
+ stdout_truncated: result.stdoutTruncated,
164
+ stderr_truncated: result.stderrTruncated,
165
+ };
166
+ writeSummary(logPaths.assessment, summary);
167
+ return summary;
168
+ }
169
+
170
+ // ── 8. Handle non-zero exit ──
171
+ if (result.exitCode !== 0) {
172
+ const summary: AgentAssessmentSummary = {
173
+ ...emptySummary,
174
+ attempted: true,
175
+ status: "non_zero_exit",
176
+ merged_risk: "medium",
177
+ merged_decision: "needs_confirm",
178
+ merged_reason_codes: ["agent_assessment_non_zero_exit"],
179
+ exit_code: result.exitCode,
180
+ stdout_truncated: result.stdoutTruncated,
181
+ stderr_truncated: result.stderrTruncated,
182
+ };
183
+ writeSummary(logPaths.assessment, summary);
184
+ return summary;
185
+ }
186
+
187
+ // ── 9. Parse JSON from stdout ──
188
+ const parsed = parseAssessmentJson(result.stdout, input.repoPath);
189
+
190
+ if (!parsed.output) {
191
+ const summary: AgentAssessmentSummary = {
192
+ ...emptySummary,
193
+ attempted: true,
194
+ status: "parse_failed",
195
+ merged_risk: "medium",
196
+ merged_decision: "needs_confirm",
197
+ merged_reason_codes: ["agent_assessment_parse_failed", ...parsed.sanitized_reasons],
198
+ exit_code: result.exitCode,
199
+ stdout_truncated: result.stdoutTruncated,
200
+ stderr_truncated: result.stderrTruncated,
201
+ };
202
+ writeSummary(logPaths.assessment, summary);
203
+ return summary;
204
+ }
205
+
206
+ // ── 10. Merge risk (agent can only raise, not lower) ──
207
+ const agentRisk = parsed.output.risk_level;
208
+ let mergedRisk: "low" | "medium" | "high" = agentRisk === "high" ? "high" : agentRisk === "medium" ? "medium" : "low";
209
+ let mergedDecision: "allow" | "needs_confirm" | "blocked" =
210
+ mergedRisk === "high" ? "blocked" : mergedRisk === "medium" ? "needs_confirm" : "allow";
211
+ const mergedReasonCodes = ["agent_assessment_completed", ...parsed.sanitized_reasons];
212
+ // 修复 #5: agent requires_user_confirm=true 时至少升级为 medium / needs_confirm
213
+ if (parsed.output.requires_user_confirm && mergedRisk === "low") {
214
+ mergedRisk = "medium";
215
+ mergedDecision = "needs_confirm";
216
+ mergedReasonCodes.push("agent_requested_confirm");
217
+ }
218
+
219
+ const summary: AgentAssessmentSummary = {
220
+ attempted: true,
221
+ status: "completed",
222
+ output: { ...parsed.output, assessed_at: new Date().toISOString() },
223
+ merged_risk: mergedRisk,
224
+ merged_decision: mergedDecision,
225
+ merged_reason_codes: mergedReasonCodes,
226
+ timed_out: false,
227
+ exit_code: result.exitCode,
228
+ read_only_violation: false,
229
+ violation_files: [],
230
+ stdout_truncated: result.stdoutTruncated,
231
+ stderr_truncated: result.stderrTruncated,
232
+ log_paths: logPaths,
233
+ };
234
+ writeSummary(logPaths.assessment, summary);
235
+ return summary;
236
+ }
237
+
238
+ interface ParsedAssessment {
239
+ output: AgentAssessmentOutput | null;
240
+ sanitized_reasons: string[];
241
+ }
242
+
243
+ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment {
244
+ const sanitized_reasons: string[] = [];
245
+
246
+ // Use the LAST marker only
247
+ const lastMarkerIndex = stdout.lastIndexOf(ASSESSMENT_MARKER);
248
+ if (lastMarkerIndex < 0) return { output: null, sanitized_reasons };
249
+
250
+ const jsonText = stdout.slice(lastMarkerIndex + ASSESSMENT_MARKER.length).trim();
251
+ // Find the JSON object (from first { to last })
252
+ const jsonStart = jsonText.indexOf("{");
253
+ const jsonEnd = jsonText.lastIndexOf("}");
254
+ if (jsonStart < 0 || jsonEnd < 0 || jsonEnd <= jsonStart) return { output: null, sanitized_reasons };
255
+
256
+ let parsed: any;
257
+ try {
258
+ parsed = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
259
+ } catch {
260
+ return { output: null, sanitized_reasons };
261
+ }
262
+
263
+ // ── Strict validation ──
264
+ if (parsed.risk_level !== "low" && parsed.risk_level !== "medium" && parsed.risk_level !== "high") {
265
+ return { output: null, sanitized_reasons };
266
+ }
267
+
268
+ const confidence = Number(parsed.confidence);
269
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
270
+ return { output: null, sanitized_reasons };
271
+ }
272
+
273
+ if (!Array.isArray(parsed.reason_codes)) return { output: null, sanitized_reasons };
274
+ if (parsed.reason_codes.length > 50) return { output: null, sanitized_reasons };
275
+ const reasonCodes = parsed.reason_codes.filter((r: any) => typeof r === "string" && r.length <= 100);
276
+ if (reasonCodes.length !== parsed.reason_codes.length) {
277
+ sanitized_reasons.push("reason_codes_filtered");
278
+ }
279
+
280
+ if (!Array.isArray(parsed.affected_paths)) return { output: null, sanitized_reasons };
281
+ if (parsed.affected_paths.length > 100) return { output: null, sanitized_reasons };
282
+ const affectedPaths: string[] = [];
283
+ let pathsSanitized = false;
284
+ for (const p of parsed.affected_paths) {
285
+ if (typeof p !== "string") { pathsSanitized = true; continue; }
286
+ // Must be relative, not absolute, and within repo
287
+ if (isAbsolute(p)) { pathsSanitized = true; continue; }
288
+ const resolved = resolve(repoPath, p);
289
+ const rel = relative(repoPath, resolved);
290
+ if (rel.startsWith("..")) { pathsSanitized = true; continue; }
291
+ affectedPaths.push(p);
292
+ }
293
+ if (pathsSanitized) {
294
+ sanitized_reasons.push("paths_sanitized");
295
+ }
296
+
297
+ if (!Array.isArray(parsed.destructive_actions)) return { output: null, sanitized_reasons };
298
+ if (parsed.destructive_actions.length > 20) return { output: null, sanitized_reasons };
299
+ const destructiveActions = parsed.destructive_actions.filter(
300
+ (d: any) => typeof d === "string" && d.length <= 200
301
+ );
302
+
303
+ if (typeof parsed.requires_user_confirm !== "boolean") return { output: null, sanitized_reasons };
304
+
305
+ const notes = typeof parsed.notes === "string" ? parsed.notes.slice(0, 2000) : "";
306
+
307
+ return {
308
+ output: {
309
+ risk_level: parsed.risk_level,
310
+ reason_codes: reasonCodes,
311
+ affected_paths: affectedPaths,
312
+ destructive_actions: destructiveActions,
313
+ requires_user_confirm: parsed.requires_user_confirm,
314
+ confidence,
315
+ notes,
316
+ assessed_at: new Date().toISOString(),
317
+ },
318
+ sanitized_reasons,
319
+ };
320
+ }
321
+
322
+ function writeSummary(path: string, summary: AgentAssessmentSummary): void {
323
+ writeFileSync(path, JSON.stringify(summary, null, 2), "utf-8");
324
+ }