patchwarden 0.4.0 → 0.6.1
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/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
package/dist/doctor.js
CHANGED
|
@@ -17,7 +17,7 @@ import { isSensitivePath } from "./security/sensitiveGuard.js";
|
|
|
17
17
|
import { guardPlanContent } from "./security/planGuard.js";
|
|
18
18
|
import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
|
|
19
19
|
import { getToolDefs } from "./tools/registry.js";
|
|
20
|
-
import { CHATGPT_CORE_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
20
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
21
21
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
22
22
|
// ── State ──────────────────────────────────────────────────────────
|
|
23
23
|
let ok = 0;
|
|
@@ -59,6 +59,7 @@ function cmd(cmdStr) {
|
|
|
59
59
|
// ══════════════════════════════════════════════════════════════════
|
|
60
60
|
async function main() {
|
|
61
61
|
console.log("PatchWarden Doctor\n");
|
|
62
|
+
const allowDefaultConfig = process.argv.includes("--allow-default-config");
|
|
62
63
|
// 1. Node version
|
|
63
64
|
const nodeVer = process.version;
|
|
64
65
|
const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
|
|
@@ -66,6 +67,16 @@ async function main() {
|
|
|
66
67
|
// 2. npm
|
|
67
68
|
const npmVer = cmd("npm --version");
|
|
68
69
|
check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
|
|
70
|
+
// Local-only medium-risk confirmation entrypoint. It must exist as a package
|
|
71
|
+
// binary but must not be exposed as an MCP tool.
|
|
72
|
+
try {
|
|
73
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
74
|
+
check("patchwarden-confirm package binary", packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js", packageJson.bin?.["patchwarden-confirm"] || "missing");
|
|
75
|
+
check("patchwarden-confirm compiled entrypoint", existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")), "run npm.cmd run build");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
check("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error));
|
|
79
|
+
}
|
|
69
80
|
// 3. Git
|
|
70
81
|
const gitVer = cmd("git --version");
|
|
71
82
|
warnCheck("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work");
|
|
@@ -81,9 +92,15 @@ async function main() {
|
|
|
81
92
|
break;
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
|
-
|
|
95
|
+
const configDetail = configPathUsed
|
|
85
96
|
? configPathUsed
|
|
86
|
-
: 'Create one: cp examples/config.example.json patchwarden.config.json'
|
|
97
|
+
: 'Create one: cp examples/config.example.json patchwarden.config.json';
|
|
98
|
+
if (allowDefaultConfig) {
|
|
99
|
+
warnCheck("Config file exists", configPathUsed !== "", configDetail);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
check("Config file exists", configPathUsed !== "", configDetail);
|
|
103
|
+
}
|
|
87
104
|
// 5. PATCHWARDEN_CONFIG env
|
|
88
105
|
if (process.env.PATCHWARDEN_CONFIG) {
|
|
89
106
|
results.push(`[OK] PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`);
|
|
@@ -198,15 +215,26 @@ async function main() {
|
|
|
198
215
|
try {
|
|
199
216
|
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
200
217
|
const fullTools = getToolDefs();
|
|
201
|
-
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core");
|
|
218
|
+
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
|
|
202
219
|
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
|
|
203
220
|
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
|
|
204
|
-
check("Full tool profile exposes
|
|
221
|
+
check("Full tool profile exposes 30 tools", fullTools.length === 30, `${fullTools.length} tools`);
|
|
205
222
|
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
223
|
check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
|
|
207
224
|
createSchema?.properties?.verify_commands &&
|
|
208
225
|
waitSchema?.properties?.timeout_seconds &&
|
|
209
226
|
waitSchema?.properties?.wait_seconds));
|
|
227
|
+
// chatgpt_direct profile checks
|
|
228
|
+
const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
|
|
229
|
+
check("chatgpt_direct disabled exposes only health_check (degraded mode)", directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check", `${directDisabledTools.map((t) => t.name).join(", ")}`);
|
|
230
|
+
if (config?.enableDirectProfile) {
|
|
231
|
+
const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
|
|
232
|
+
check(`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`, JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES), directEnabledTools.map((tool) => tool.name).join(", "));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
|
|
236
|
+
ok++;
|
|
237
|
+
}
|
|
210
238
|
}
|
|
211
239
|
finally {
|
|
212
240
|
if (previousProfile === undefined)
|
|
@@ -259,6 +287,31 @@ async function main() {
|
|
|
259
287
|
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
260
288
|
check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
261
289
|
}
|
|
290
|
+
// Direct tool module checks
|
|
291
|
+
const directToolModules = [
|
|
292
|
+
"createDirectSession",
|
|
293
|
+
"searchWorkspace",
|
|
294
|
+
"applyPatch",
|
|
295
|
+
"runVerification",
|
|
296
|
+
"finalizeDirectSession",
|
|
297
|
+
"auditSession",
|
|
298
|
+
];
|
|
299
|
+
for (const t of directToolModules) {
|
|
300
|
+
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
301
|
+
check(`Direct tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
302
|
+
}
|
|
303
|
+
// Direct support module checks
|
|
304
|
+
const directSupportModules = [
|
|
305
|
+
"directSessionStore",
|
|
306
|
+
"directGuards",
|
|
307
|
+
"directPatch",
|
|
308
|
+
"directVerification",
|
|
309
|
+
"directAudit",
|
|
310
|
+
];
|
|
311
|
+
for (const t of directSupportModules) {
|
|
312
|
+
const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
|
|
313
|
+
check(`Direct support module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
314
|
+
}
|
|
262
315
|
// Task directory writable
|
|
263
316
|
if (config) {
|
|
264
317
|
const tasksDir = resolve(config.workspaceRoot, config.tasksDir);
|
|
@@ -334,11 +387,26 @@ async function main() {
|
|
|
334
387
|
}
|
|
335
388
|
}
|
|
336
389
|
}
|
|
390
|
+
// 14b. Direct profile config checks
|
|
391
|
+
if (config) {
|
|
392
|
+
const directCmds = config.directAllowedCommands || [];
|
|
393
|
+
warnCheck("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured");
|
|
394
|
+
// npm run doctor should NOT be in default Direct whitelist
|
|
395
|
+
const hasDoctor = directCmds.some((c) => c === "npm run doctor");
|
|
396
|
+
check("directAllowedCommands does not include npm run doctor", !hasDoctor, hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present");
|
|
397
|
+
check("directSessionTtlSeconds is valid", config.directSessionTtlSeconds >= 60 && config.directSessionTtlSeconds <= 86400, `${config.directSessionTtlSeconds}s`);
|
|
398
|
+
check("directMaxPatchBytes is positive", config.directMaxPatchBytes > 0, `${config.directMaxPatchBytes}`);
|
|
399
|
+
check("directMaxFileBytes is positive", config.directMaxFileBytes > 0, `${config.directMaxFileBytes}`);
|
|
400
|
+
}
|
|
337
401
|
// 15. Tunnel example files check
|
|
338
402
|
const tunnelFiles = [
|
|
339
403
|
"examples/openai-tunnel/README.md",
|
|
340
404
|
"examples/openai-tunnel/tunnel-client.example.yaml",
|
|
341
405
|
"examples/openai-tunnel/chatgpt-test-prompt.md",
|
|
406
|
+
"scripts/patchwarden-mcp-direct.cmd",
|
|
407
|
+
"PatchWarden.cmd",
|
|
408
|
+
"scripts/manage-patchwarden.ps1",
|
|
409
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
342
410
|
];
|
|
343
411
|
for (const tf of tunnelFiles) {
|
|
344
412
|
const full = resolve(process.cwd(), tf);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type LogLevel = "info" | "warn" | "error" | "audit";
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
level: LogLevel;
|
|
5
|
+
message: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns true when PATCHWARDEN_VERBOSE_LOG is set to "true".
|
|
10
|
+
* Verbose mode enables logging of sanitized tool-call arguments.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isVerboseLogging(): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Structured logger that emits JSON lines to stderr.
|
|
15
|
+
*
|
|
16
|
+
* Each entry contains `timestamp`, `level`, `message`, plus any optional
|
|
17
|
+
* context fields supplied by the caller.
|
|
18
|
+
*/
|
|
19
|
+
export declare class Logger {
|
|
20
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
21
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
22
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
23
|
+
/**
|
|
24
|
+
* Emit a tool-call audit log entry.
|
|
25
|
+
*
|
|
26
|
+
* Required fields: `tool`, `ok`, `duration_ms`.
|
|
27
|
+
* Optional fields: `error_reason`, `task_id`.
|
|
28
|
+
*
|
|
29
|
+
* By default raw arguments are NOT logged. When verbose mode is enabled
|
|
30
|
+
* (PATCHWARDEN_VERBOSE_LOG=true) and `args` is provided, the arguments
|
|
31
|
+
* are serialized and sanitized via `redactSensitiveContent` before being
|
|
32
|
+
* included in the `args` field.
|
|
33
|
+
*/
|
|
34
|
+
audit(tool: string, ok: boolean, durationMs: number, errorReason?: string, taskId?: string, args?: unknown): void;
|
|
35
|
+
}
|
|
36
|
+
/** Default singleton logger instance. */
|
|
37
|
+
export declare const logger: Logger;
|
|
38
|
+
/**
|
|
39
|
+
* Produce a structured error log entry for an unhandled rejection or
|
|
40
|
+
* uncaught exception. Writes JSON to stderr.
|
|
41
|
+
*/
|
|
42
|
+
export declare function logUnhandledError(error: unknown): void;
|
|
43
|
+
/**
|
|
44
|
+
* Register process-level handlers for `unhandledRejection` and
|
|
45
|
+
* `uncaughtException`.
|
|
46
|
+
*
|
|
47
|
+
* Both handlers log structured error output to stderr. The
|
|
48
|
+
* `uncaughtException` handler does NOT swallow the fatal error — after
|
|
49
|
+
* logging it exits with a non-zero status code to preserve the default
|
|
50
|
+
* crash behaviour required for truly fatal failures.
|
|
51
|
+
*/
|
|
52
|
+
export declare function installGlobalHandlers(): void;
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
2
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Returns true when PATCHWARDEN_VERBOSE_LOG is set to "true".
|
|
5
|
+
* Verbose mode enables logging of sanitized tool-call arguments.
|
|
6
|
+
*/
|
|
7
|
+
export function isVerboseLogging() {
|
|
8
|
+
return process.env.PATCHWARDEN_VERBOSE_LOG === "true";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Serialize a value to a JSON string, falling back to String() on failure
|
|
12
|
+
* (e.g. circular references, functions, symbols).
|
|
13
|
+
*/
|
|
14
|
+
function safeStringify(value) {
|
|
15
|
+
try {
|
|
16
|
+
const result = JSON.stringify(value);
|
|
17
|
+
return result === undefined ? String(value) : result;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Write a single JSON log line to stderr.
|
|
25
|
+
*
|
|
26
|
+
* All log output goes to stderr — NEVER stdout — so that MCP JSON-RPC
|
|
27
|
+
* traffic on stdout is never polluted by log messages.
|
|
28
|
+
*/
|
|
29
|
+
function emit(entry) {
|
|
30
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
31
|
+
}
|
|
32
|
+
// ── Logger ────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Structured logger that emits JSON lines to stderr.
|
|
35
|
+
*
|
|
36
|
+
* Each entry contains `timestamp`, `level`, `message`, plus any optional
|
|
37
|
+
* context fields supplied by the caller.
|
|
38
|
+
*/
|
|
39
|
+
export class Logger {
|
|
40
|
+
info(message, context) {
|
|
41
|
+
emit({ timestamp: new Date().toISOString(), level: "info", message, ...context });
|
|
42
|
+
}
|
|
43
|
+
warn(message, context) {
|
|
44
|
+
emit({ timestamp: new Date().toISOString(), level: "warn", message, ...context });
|
|
45
|
+
}
|
|
46
|
+
error(message, context) {
|
|
47
|
+
emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Emit a tool-call audit log entry.
|
|
51
|
+
*
|
|
52
|
+
* Required fields: `tool`, `ok`, `duration_ms`.
|
|
53
|
+
* Optional fields: `error_reason`, `task_id`.
|
|
54
|
+
*
|
|
55
|
+
* By default raw arguments are NOT logged. When verbose mode is enabled
|
|
56
|
+
* (PATCHWARDEN_VERBOSE_LOG=true) and `args` is provided, the arguments
|
|
57
|
+
* are serialized and sanitized via `redactSensitiveContent` before being
|
|
58
|
+
* included in the `args` field.
|
|
59
|
+
*/
|
|
60
|
+
audit(tool, ok, durationMs, errorReason, taskId, args) {
|
|
61
|
+
const entry = {
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
level: "audit",
|
|
64
|
+
message: "tool_call_audit",
|
|
65
|
+
tool,
|
|
66
|
+
ok,
|
|
67
|
+
duration_ms: durationMs,
|
|
68
|
+
};
|
|
69
|
+
if (errorReason !== undefined)
|
|
70
|
+
entry.error_reason = errorReason;
|
|
71
|
+
if (taskId !== undefined)
|
|
72
|
+
entry.task_id = taskId;
|
|
73
|
+
if (args !== undefined && isVerboseLogging()) {
|
|
74
|
+
const redacted = redactSensitiveContent(safeStringify(args));
|
|
75
|
+
entry.args = redacted.content;
|
|
76
|
+
}
|
|
77
|
+
emit(entry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Default singleton logger instance. */
|
|
81
|
+
export const logger = new Logger();
|
|
82
|
+
// ── Unhandled error helpers ───────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Produce a structured error log entry for an unhandled rejection or
|
|
85
|
+
* uncaught exception. Writes JSON to stderr.
|
|
86
|
+
*/
|
|
87
|
+
export function logUnhandledError(error) {
|
|
88
|
+
const entry = {
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
level: "error",
|
|
91
|
+
message: "unhandled_error",
|
|
92
|
+
error: error instanceof Error
|
|
93
|
+
? error.message
|
|
94
|
+
: typeof error === "string"
|
|
95
|
+
? error
|
|
96
|
+
: safeStringify(error),
|
|
97
|
+
error_name: error instanceof Error ? error.name : typeof error,
|
|
98
|
+
};
|
|
99
|
+
if (error instanceof Error && error.stack) {
|
|
100
|
+
entry.stack = error.stack;
|
|
101
|
+
}
|
|
102
|
+
emit(entry);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Register process-level handlers for `unhandledRejection` and
|
|
106
|
+
* `uncaughtException`.
|
|
107
|
+
*
|
|
108
|
+
* Both handlers log structured error output to stderr. The
|
|
109
|
+
* `uncaughtException` handler does NOT swallow the fatal error — after
|
|
110
|
+
* logging it exits with a non-zero status code to preserve the default
|
|
111
|
+
* crash behaviour required for truly fatal failures.
|
|
112
|
+
*/
|
|
113
|
+
export function installGlobalHandlers() {
|
|
114
|
+
process.on("unhandledRejection", (reason) => {
|
|
115
|
+
logUnhandledError(reason);
|
|
116
|
+
});
|
|
117
|
+
process.on("uncaughtException", (error) => {
|
|
118
|
+
logUnhandledError(error);
|
|
119
|
+
// Do not swallow: exit with failure so the process does not continue
|
|
120
|
+
// in an undefined state.
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PatchWardenConfig } from "../config.js";
|
|
2
|
+
export interface AgentInvocation {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
commandLabel: string;
|
|
7
|
+
promptMode: "inline" | "file";
|
|
8
|
+
promptFilePath?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build agent invocation parameters from config.
|
|
12
|
+
* Replaces {repo}, {prompt}, and {prompt_file} placeholders.
|
|
13
|
+
* runTask and agentAssessor share this function to ensure consistent agent startup.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildAgentInvocation(agentName: string, repoPath: string, prompt: string, config: PatchWardenConfig, promptFilePath?: string): AgentInvocation;
|
|
16
|
+
/**
|
|
17
|
+
* Build task execution prompt. Mechanically extracted from runTask.ts.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildExecutionPrompt(plan: string, repoPath: string, testCommand: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Build agentAssessor inspect-only prompt.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildAssessmentPrompt(goal: string, planContent: string, repoPath: string): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { guardAgentCommand, sanitizePromptArg } from "../security/commandGuard.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build agent invocation parameters from config.
|
|
5
|
+
* Replaces {repo}, {prompt}, and {prompt_file} placeholders.
|
|
6
|
+
* runTask and agentAssessor share this function to ensure consistent agent startup.
|
|
7
|
+
*/
|
|
8
|
+
export function buildAgentInvocation(agentName, repoPath, prompt, config, promptFilePath) {
|
|
9
|
+
const agentCmd = guardAgentCommand(agentName, config);
|
|
10
|
+
const sanitizedPrompt = sanitizePromptArg(prompt);
|
|
11
|
+
const hasPromptFilePlaceholder = agentCmd.args.includes("{prompt_file}");
|
|
12
|
+
const promptMode = hasPromptFilePlaceholder && promptFilePath ? "file" : "inline";
|
|
13
|
+
const resolvedArgs = agentCmd.args.map((arg) => {
|
|
14
|
+
if (arg === "{repo}")
|
|
15
|
+
return repoPath;
|
|
16
|
+
if (arg === "{prompt}")
|
|
17
|
+
return sanitizedPrompt;
|
|
18
|
+
if (arg === "{prompt_file}" && promptMode === "file" && promptFilePath)
|
|
19
|
+
return promptFilePath;
|
|
20
|
+
return arg;
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
command: agentCmd.command,
|
|
24
|
+
args: resolvedArgs,
|
|
25
|
+
cwd: repoPath,
|
|
26
|
+
commandLabel: `${basename(agentCmd.command)} (configured agent command)`,
|
|
27
|
+
promptMode,
|
|
28
|
+
...(promptMode === "file" && promptFilePath ? { promptFilePath } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build task execution prompt. Mechanically extracted from runTask.ts.
|
|
33
|
+
*/
|
|
34
|
+
export function buildExecutionPrompt(plan, repoPath, testCommand) {
|
|
35
|
+
let prompt = `You are executing a pre-written plan in a local repository.
|
|
36
|
+
|
|
37
|
+
## Repository
|
|
38
|
+
${repoPath}
|
|
39
|
+
|
|
40
|
+
## Plan
|
|
41
|
+
${plan}
|
|
42
|
+
|
|
43
|
+
## Instructions
|
|
44
|
+
1. Read the plan carefully.
|
|
45
|
+
2. Implement the changes in this repository only.
|
|
46
|
+
3. Do NOT modify files outside this repository.
|
|
47
|
+
4. Leave repository changes uncommitted for review; remote operations are outside this task.
|
|
48
|
+
5. After implementing, describe what you changed.
|
|
49
|
+
6. Output a summary with what was done, files modified, and issues encountered.
|
|
50
|
+
`;
|
|
51
|
+
if (testCommand) {
|
|
52
|
+
prompt += `\n7. You may run ${testCommand}; PatchWarden will independently run it again for verification.`;
|
|
53
|
+
}
|
|
54
|
+
return prompt;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build agentAssessor inspect-only prompt.
|
|
58
|
+
*/
|
|
59
|
+
export function buildAssessmentPrompt(goal, planContent, repoPath) {
|
|
60
|
+
return `You are performing a READ-ONLY risk assessment of a planned task. Do NOT modify any files.
|
|
61
|
+
|
|
62
|
+
## Repository
|
|
63
|
+
${repoPath}
|
|
64
|
+
|
|
65
|
+
## Goal
|
|
66
|
+
${goal}
|
|
67
|
+
|
|
68
|
+
## Plan
|
|
69
|
+
${planContent}
|
|
70
|
+
|
|
71
|
+
## Instructions
|
|
72
|
+
1. Read the plan and goal carefully.
|
|
73
|
+
2. Inspect the repository to understand the scope of changes.
|
|
74
|
+
3. Do NOT create, edit, delete, rename, or generate any files.
|
|
75
|
+
4. Assess the risk level of executing this plan.
|
|
76
|
+
5. Identify affected file paths within the repository.
|
|
77
|
+
6. Check for destructive actions, sensitive file access, or out-of-scope changes.
|
|
78
|
+
7. Output your assessment as JSON after the marker ===ASSESSMENT_JSON=== on a new line.
|
|
79
|
+
|
|
80
|
+
## Required JSON output format
|
|
81
|
+
After your analysis, output exactly this format:
|
|
82
|
+
|
|
83
|
+
===ASSESSMENT_JSON===
|
|
84
|
+
{
|
|
85
|
+
"risk_level": "low" | "medium" | "high",
|
|
86
|
+
"reason_codes": ["short", "descriptive", "codes"],
|
|
87
|
+
"affected_paths": ["relative/path/to/file"],
|
|
88
|
+
"destructive_actions": ["description of any destructive actions"],
|
|
89
|
+
"requires_user_confirm": false,
|
|
90
|
+
"confidence": 0.0,
|
|
91
|
+
"notes": "Brief summary of findings"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
## Risk level guidelines
|
|
95
|
+
- low: Small source change, no sensitive files, no destructive actions
|
|
96
|
+
- medium: Multiple files, build artifacts, dependency changes, or uncertain scope
|
|
97
|
+
- high: Destructive actions, sensitive file access, out-of-scope changes, or large-scale deletion
|
|
98
|
+
|
|
99
|
+
## Constraints
|
|
100
|
+
- affected_paths must be relative paths within the repository (no absolute paths)
|
|
101
|
+
- reason_codes: max 50 entries, each max 100 chars
|
|
102
|
+
- destructive_actions: max 20 entries, each max 200 chars
|
|
103
|
+
- notes: max 2000 chars
|
|
104
|
+
- confidence: number from 0.0 to 1.0
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface FileFingerprint {
|
|
2
2
|
size: number;
|
|
3
3
|
sha256: string;
|
|
4
|
+
tracked: boolean;
|
|
5
|
+
ignored: boolean;
|
|
4
6
|
}
|
|
5
7
|
export interface RepoSnapshot {
|
|
6
8
|
captured_at: string;
|
|
@@ -9,6 +11,7 @@ export interface RepoSnapshot {
|
|
|
9
11
|
status: string;
|
|
10
12
|
workspace_dirty: boolean;
|
|
11
13
|
files: Record<string, FileFingerprint>;
|
|
14
|
+
dirty_paths: string[];
|
|
12
15
|
warnings: string[];
|
|
13
16
|
}
|
|
14
17
|
export interface ChangedFile {
|
|
@@ -17,6 +20,31 @@ export interface ChangedFile {
|
|
|
17
20
|
old_path?: string;
|
|
18
21
|
before_sha256: string | null;
|
|
19
22
|
after_sha256: string | null;
|
|
23
|
+
tracked: boolean;
|
|
24
|
+
ignored: boolean;
|
|
25
|
+
kind: "source" | "build_artifact" | "runtime_generated";
|
|
26
|
+
}
|
|
27
|
+
export interface ClassifiedChange {
|
|
28
|
+
path: string;
|
|
29
|
+
change: ChangedFile["change"];
|
|
30
|
+
tracked: boolean;
|
|
31
|
+
ignored: boolean;
|
|
32
|
+
kind: ChangedFile["kind"];
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ArtifactHygiene {
|
|
36
|
+
counts: {
|
|
37
|
+
source_changes: number;
|
|
38
|
+
tracked_build_artifacts: number;
|
|
39
|
+
ignored_untracked_artifacts: number;
|
|
40
|
+
runtime_generated_files: number;
|
|
41
|
+
suspicious_changes: number;
|
|
42
|
+
};
|
|
43
|
+
source_changes: ClassifiedChange[];
|
|
44
|
+
tracked_build_artifacts: ClassifiedChange[];
|
|
45
|
+
ignored_untracked_artifacts: ClassifiedChange[];
|
|
46
|
+
runtime_generated_files: ClassifiedChange[];
|
|
47
|
+
suspicious_changes: ClassifiedChange[];
|
|
20
48
|
}
|
|
21
49
|
export interface ChangeArtifacts {
|
|
22
50
|
changed_files: ChangedFile[];
|
|
@@ -36,8 +64,50 @@ export interface ChangeArtifacts {
|
|
|
36
64
|
workspace_dirty_after: boolean;
|
|
37
65
|
patch_mode: "textual" | "no_changes" | "hash_only";
|
|
38
66
|
unavailable_reason: string | null;
|
|
67
|
+
artifact_hygiene: ArtifactHygiene;
|
|
39
68
|
}
|
|
40
69
|
export declare function captureRepoSnapshot(repoPath: string): RepoSnapshot;
|
|
41
70
|
export declare function writeSnapshot(taskDir: string, filename: string, snapshot: RepoSnapshot): void;
|
|
42
71
|
export declare function buildChangeArtifacts(repoPath: string, before: RepoSnapshot, after: RepoSnapshot): ChangeArtifacts;
|
|
43
72
|
export declare function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): ChangedFile[];
|
|
73
|
+
export declare function emptyArtifactHygiene(): ArtifactHygiene;
|
|
74
|
+
export interface ExternalDirtyFile {
|
|
75
|
+
path: string;
|
|
76
|
+
change: ChangedFile["change"];
|
|
77
|
+
before_sha256: string | null;
|
|
78
|
+
after_sha256: string | null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract files that are dirty in the workspace but outside the target repo.
|
|
82
|
+
* Used to establish a baseline before task execution.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extractExternalDirtyFiles(workspaceSnapshot: RepoSnapshot, repoPath: string, workspaceRoot: string): ExternalDirtyFile[];
|
|
85
|
+
/**
|
|
86
|
+
* Compare external dirty files between baseline and post-task snapshots.
|
|
87
|
+
* Returns files that are NEW (not present in baseline) or CHANGED
|
|
88
|
+
* (same path but different sha256, meaning the task modified them).
|
|
89
|
+
*/
|
|
90
|
+
export declare function findNewExternalDirtyFiles(baseline: ExternalDirtyFile[], current: ExternalDirtyFile[]): ExternalDirtyFile[];
|
|
91
|
+
export interface ArtifactManifestEntry {
|
|
92
|
+
path: string;
|
|
93
|
+
type: string;
|
|
94
|
+
size: number;
|
|
95
|
+
sha256: string;
|
|
96
|
+
generated_by: string;
|
|
97
|
+
created_at: string;
|
|
98
|
+
}
|
|
99
|
+
export interface ArtifactManifest {
|
|
100
|
+
task_id: string | null;
|
|
101
|
+
generated_at: string;
|
|
102
|
+
artifacts: ArtifactManifestEntry[];
|
|
103
|
+
}
|
|
104
|
+
export declare function buildArtifactManifest(changedFiles: ChangedFile[], repoPath: string, taskId?: string): ArtifactManifest;
|
|
105
|
+
export interface ChangedFileGroups {
|
|
106
|
+
source_changes: ChangedFile[];
|
|
107
|
+
docs_changes: ChangedFile[];
|
|
108
|
+
config_changes: ChangedFile[];
|
|
109
|
+
test_changes: ChangedFile[];
|
|
110
|
+
release_artifacts: ChangedFile[];
|
|
111
|
+
runtime_generated_files: ChangedFile[];
|
|
112
|
+
}
|
|
113
|
+
export declare function groupChangedFiles(changedFiles: ChangedFile[]): ChangedFileGroups;
|