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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import {
|
|
7
|
+
diagnoseAndroidBuild,
|
|
8
|
+
type AndroidBuildDiagnosticReport,
|
|
9
|
+
} from "../../tools/androidDoctor.js";
|
|
10
|
+
|
|
11
|
+
const VALID_STATUSES = ["ok", "warn", "fail", "skip"] as const;
|
|
12
|
+
|
|
13
|
+
/** Restore an env var to its original value (deleted when undefined). */
|
|
14
|
+
function restoreEnv(key: string, value: string | undefined) {
|
|
15
|
+
if (value === undefined) delete process.env[key];
|
|
16
|
+
else process.env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("diagnoseAndroidBuild", () => {
|
|
20
|
+
let tempDir: string;
|
|
21
|
+
let savedJavaHome: string | undefined;
|
|
22
|
+
let savedAndroidHome: string | undefined;
|
|
23
|
+
let savedAndroidSdkRoot: string | undefined;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-android-doctor-"));
|
|
27
|
+
savedJavaHome = process.env.JAVA_HOME;
|
|
28
|
+
savedAndroidHome = process.env.ANDROID_HOME;
|
|
29
|
+
savedAndroidSdkRoot = process.env.ANDROID_SDK_ROOT;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
34
|
+
restoreEnv("JAVA_HOME", savedJavaHome);
|
|
35
|
+
restoreEnv("ANDROID_HOME", savedAndroidHome);
|
|
36
|
+
restoreEnv("ANDROID_SDK_ROOT", savedAndroidSdkRoot);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns a skip result when no android_app directory exists", () => {
|
|
40
|
+
const result = diagnoseAndroidBuild(tempDir);
|
|
41
|
+
assert.equal(result.status, "skip");
|
|
42
|
+
assert.equal((result as { reason: string }).reason, "No android_app directory found");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns a structured diagnostic report when android_app exists", () => {
|
|
46
|
+
const androidApp = join(tempDir, "android_app");
|
|
47
|
+
mkdirSync(join(androidApp, "app"), { recursive: true });
|
|
48
|
+
mkdirSync(join(androidApp, "gradle", "wrapper"), { recursive: true });
|
|
49
|
+
writeFileSync(join(androidApp, "gradlew"), "#!/bin/sh\necho gradle\n");
|
|
50
|
+
writeFileSync(join(androidApp, "settings.gradle.kts"), 'pluginManagement { repositories { google() } }\n');
|
|
51
|
+
writeFileSync(join(androidApp, "app", "build.gradle.kts"), 'plugins { id("com.android.application") }\n');
|
|
52
|
+
writeFileSync(
|
|
53
|
+
join(androidApp, "gradle", "wrapper", "gradle-wrapper.properties"),
|
|
54
|
+
"distributionUrl=https://services.gradle.org/distributions/gradle-8.5-bin.zip\n",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Make SDK-related checks deterministic regardless of the host environment.
|
|
58
|
+
delete process.env.ANDROID_HOME;
|
|
59
|
+
delete process.env.ANDROID_SDK_ROOT;
|
|
60
|
+
|
|
61
|
+
const result = diagnoseAndroidBuild(tempDir);
|
|
62
|
+
assert.notEqual(result.status, "skip");
|
|
63
|
+
const report = result as AndroidBuildDiagnosticReport;
|
|
64
|
+
|
|
65
|
+
assert.equal(report.repo_path, tempDir);
|
|
66
|
+
assert.equal(report.android_app_path, join(tempDir, "android_app"));
|
|
67
|
+
assert.ok(Array.isArray(report.checks));
|
|
68
|
+
assert.equal(report.checks.length, 10);
|
|
69
|
+
|
|
70
|
+
for (const item of report.checks) {
|
|
71
|
+
assert.equal(typeof item.check, "string");
|
|
72
|
+
assert.ok(item.check.length > 0);
|
|
73
|
+
assert.ok((VALID_STATUSES as readonly string[]).includes(item.status));
|
|
74
|
+
assert.equal(typeof item.reason, "string");
|
|
75
|
+
assert.equal(typeof item.suggested_fix, "string");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Overall status follows the fail > warn > ok precedence.
|
|
79
|
+
const hasFail = report.checks.some((c) => c.status === "fail");
|
|
80
|
+
const hasWarn = report.checks.some((c) => c.status === "warn");
|
|
81
|
+
const expected = hasFail ? "fail" : hasWarn ? "warn" : "ok";
|
|
82
|
+
assert.equal(report.status, expected);
|
|
83
|
+
|
|
84
|
+
// The expected check names are all present.
|
|
85
|
+
const checkNames = report.checks.map((c) => c.check);
|
|
86
|
+
assert.ok(checkNames.includes("java -version"));
|
|
87
|
+
assert.ok(checkNames.includes("JAVA_HOME"));
|
|
88
|
+
assert.ok(checkNames.includes("ANDROID_HOME"));
|
|
89
|
+
assert.ok(checkNames.includes("ANDROID_SDK_ROOT"));
|
|
90
|
+
assert.ok(checkNames.includes("Android SDK platform"));
|
|
91
|
+
assert.ok(checkNames.includes("Android build-tools"));
|
|
92
|
+
assert.ok(checkNames.includes("Gradle wrapper"));
|
|
93
|
+
assert.ok(checkNames.includes("android_app/settings.gradle.kts"));
|
|
94
|
+
assert.ok(checkNames.includes("android_app/app/build.gradle.kts"));
|
|
95
|
+
assert.ok(checkNames.includes("APK output path"));
|
|
96
|
+
|
|
97
|
+
// Files we created should be detected as ok.
|
|
98
|
+
const gradleCheck = report.checks.find((c) => c.check === "Gradle wrapper");
|
|
99
|
+
assert.equal(gradleCheck?.status, "ok");
|
|
100
|
+
|
|
101
|
+
const settingsCheck = report.checks.find((c) => c.check === "android_app/settings.gradle.kts");
|
|
102
|
+
assert.equal(settingsCheck?.status, "ok");
|
|
103
|
+
|
|
104
|
+
const appBuildCheck = report.checks.find((c) => c.check === "android_app/app/build.gradle.kts");
|
|
105
|
+
assert.equal(appBuildCheck?.status, "ok");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("reports a missing JAVA_HOME as warn or fail", () => {
|
|
109
|
+
const androidApp = join(tempDir, "android_app");
|
|
110
|
+
mkdirSync(androidApp, { recursive: true });
|
|
111
|
+
|
|
112
|
+
delete process.env.JAVA_HOME;
|
|
113
|
+
delete process.env.ANDROID_HOME;
|
|
114
|
+
delete process.env.ANDROID_SDK_ROOT;
|
|
115
|
+
|
|
116
|
+
const result = diagnoseAndroidBuild(tempDir);
|
|
117
|
+
const report = result as AndroidBuildDiagnosticReport;
|
|
118
|
+
const javaHomeCheck = report.checks.find((c) => c.check === "JAVA_HOME");
|
|
119
|
+
assert.ok(javaHomeCheck, "JAVA_HOME check should exist");
|
|
120
|
+
assert.ok(
|
|
121
|
+
javaHomeCheck!.status === "warn" || javaHomeCheck!.status === "fail",
|
|
122
|
+
`expected warn or fail, got ${javaHomeCheck!.status}`,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("reports a missing ANDROID_HOME as fail with the SDK-missing message", () => {
|
|
127
|
+
const androidApp = join(tempDir, "android_app");
|
|
128
|
+
mkdirSync(androidApp, { recursive: true });
|
|
129
|
+
|
|
130
|
+
delete process.env.ANDROID_HOME;
|
|
131
|
+
delete process.env.ANDROID_SDK_ROOT;
|
|
132
|
+
|
|
133
|
+
const result = diagnoseAndroidBuild(tempDir);
|
|
134
|
+
const report = result as AndroidBuildDiagnosticReport;
|
|
135
|
+
const androidHomeCheck = report.checks.find((c) => c.check === "ANDROID_HOME");
|
|
136
|
+
assert.ok(androidHomeCheck, "ANDROID_HOME check should exist");
|
|
137
|
+
assert.equal(androidHomeCheck!.status, "fail");
|
|
138
|
+
assert.ok(
|
|
139
|
+
androidHomeCheck!.reason.includes(
|
|
140
|
+
"Android project exists, APK not built because Android SDK is missing.",
|
|
141
|
+
),
|
|
142
|
+
`reason should mention the SDK-missing sentence, got: ${androidHomeCheck!.reason}`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// With the SDK missing, the overall status must be fail.
|
|
146
|
+
assert.equal(report.status, "fail");
|
|
147
|
+
|
|
148
|
+
// The APK output check should also carry the SDK-missing reason and be skipped.
|
|
149
|
+
const apkCheck = report.checks.find((c) => c.check === "APK output path");
|
|
150
|
+
assert.ok(apkCheck, "APK output path check should exist");
|
|
151
|
+
assert.equal(apkCheck!.status, "skip");
|
|
152
|
+
assert.ok(
|
|
153
|
+
apkCheck!.reason.includes(
|
|
154
|
+
"Android project exists, APK not built because Android SDK is missing.",
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("Chinese path handling", () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-chinese-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("writes and reads status.json with Chinese repo_path", () => {
|
|
19
|
+
const chinesePath = "念念小伴_release";
|
|
20
|
+
const status = {
|
|
21
|
+
task_id: "test-001",
|
|
22
|
+
status: "pending",
|
|
23
|
+
repo_path: chinesePath,
|
|
24
|
+
resolved_repo_path: resolve(tempDir, chinesePath),
|
|
25
|
+
};
|
|
26
|
+
const statusFile = join(tempDir, "status.json");
|
|
27
|
+
writeFileSync(statusFile, JSON.stringify(status, null, 2), "utf-8");
|
|
28
|
+
|
|
29
|
+
const raw = readFileSync(statusFile, "utf-8");
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
assert.equal(parsed.repo_path, chinesePath);
|
|
32
|
+
assert.equal(parsed.resolved_repo_path, resolve(tempDir, chinesePath));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("preserves Chinese characters through JSON stringify/parse", () => {
|
|
36
|
+
const testPaths = [
|
|
37
|
+
"念念小伴_release",
|
|
38
|
+
"测试项目",
|
|
39
|
+
"项目目录/子目录",
|
|
40
|
+
"日本語プロジェクト",
|
|
41
|
+
"한국어_프로젝트",
|
|
42
|
+
];
|
|
43
|
+
for (const path of testPaths) {
|
|
44
|
+
const json = JSON.stringify({ repo_path: path });
|
|
45
|
+
const parsed = JSON.parse(json);
|
|
46
|
+
assert.equal(parsed.repo_path, path, `Path "${path}" was corrupted`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("creates and reads directories with Chinese names", () => {
|
|
51
|
+
const chineseDir = join(tempDir, "念念小伴_release");
|
|
52
|
+
mkdirSync(chineseDir, { recursive: true });
|
|
53
|
+
assert.ok(existsSync(chineseDir));
|
|
54
|
+
|
|
55
|
+
const filePath = join(chineseDir, "status.json");
|
|
56
|
+
const data = { repo_path: "念念小伴_release", status: "running" };
|
|
57
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
58
|
+
|
|
59
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
assert.equal(parsed.repo_path, "念念小伴_release");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles Chinese characters in result.md", () => {
|
|
65
|
+
const resultMd = [
|
|
66
|
+
"# PatchWarden Task Result",
|
|
67
|
+
"",
|
|
68
|
+
"## Status",
|
|
69
|
+
"done",
|
|
70
|
+
"",
|
|
71
|
+
"## Files changed",
|
|
72
|
+
"- modified: 念念小伴_release/src/main.ts",
|
|
73
|
+
"- added: 测试项目/config.json",
|
|
74
|
+
"",
|
|
75
|
+
].join("\n");
|
|
76
|
+
const resultFile = join(tempDir, "result.md");
|
|
77
|
+
writeFileSync(resultFile, resultMd, "utf-8");
|
|
78
|
+
|
|
79
|
+
const content = readFileSync(resultFile, "utf-8");
|
|
80
|
+
assert.ok(content.includes("念念小伴_release"));
|
|
81
|
+
assert.ok(content.includes("测试项目"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("handles mixed Chinese and ASCII paths", () => {
|
|
85
|
+
const mixedPath = "src/组件/Button.tsx";
|
|
86
|
+
const data = { path: mixedPath };
|
|
87
|
+
const json = JSON.stringify(data);
|
|
88
|
+
const parsed = JSON.parse(json);
|
|
89
|
+
assert.equal(parsed.path, mixedPath);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("handles Chinese characters in error messages", () => {
|
|
93
|
+
const errorMsg = 'repo_path "念念小伴_release" is outside workspace';
|
|
94
|
+
const errorFile = join(tempDir, "error.log");
|
|
95
|
+
writeFileSync(errorFile, errorMsg, "utf-8");
|
|
96
|
+
|
|
97
|
+
const content = readFileSync(errorFile, "utf-8");
|
|
98
|
+
assert.ok(content.includes("念念小伴_release"));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("handles Windows backslash paths with Chinese characters", () => {
|
|
102
|
+
const winPath = "念念小伴_release\\src\\main.ts";
|
|
103
|
+
const normalized = winPath.replace(/\\/g, "/");
|
|
104
|
+
assert.equal(normalized, "念念小伴_release/src/main.ts");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { guardAgentCommand, guardTestCommand, guardDirectCommand, sanitizePromptArg } from "../../security/commandGuard.js";
|
|
7
|
+
import { PatchWardenError } from "../../errors.js";
|
|
8
|
+
import type { PatchWardenConfig } from "../../config.js";
|
|
9
|
+
|
|
10
|
+
function makeConfig(workspaceRoot: string): PatchWardenConfig {
|
|
11
|
+
return {
|
|
12
|
+
workspaceRoot,
|
|
13
|
+
plansDir: ".patchwarden/plans",
|
|
14
|
+
tasksDir: ".patchwarden/tasks",
|
|
15
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
16
|
+
assessmentTtlSeconds: 3600,
|
|
17
|
+
agents: {
|
|
18
|
+
codex: { command: "codex", args: ["exec", "{repo}", "{prompt}"] },
|
|
19
|
+
opencode: { command: "opencode", args: ["run", "{prompt}"] },
|
|
20
|
+
},
|
|
21
|
+
allowedTestCommands: ["npm test", "npm run build", "npm run lint"],
|
|
22
|
+
repoAllowedTestCommands: {},
|
|
23
|
+
maxReadFileBytes: 200_000,
|
|
24
|
+
defaultTaskTimeoutSeconds: 900,
|
|
25
|
+
maxTaskTimeoutSeconds: 3600,
|
|
26
|
+
watcherStaleSeconds: 30,
|
|
27
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
28
|
+
directSessionTtlSeconds: 3600,
|
|
29
|
+
directMaxPatchBytes: 200_000,
|
|
30
|
+
directMaxFileBytes: 500_000,
|
|
31
|
+
directAllowedCommands: ["npm test", "npm run build"],
|
|
32
|
+
repoDirectAllowedCommands: {},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("guardTestCommand", () => {
|
|
37
|
+
let tempDir: string;
|
|
38
|
+
let config: PatchWardenConfig;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-cmdguard-"));
|
|
42
|
+
config = makeConfig(tempDir);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("allows exact match from allowlist", () => {
|
|
50
|
+
assert.equal(guardTestCommand("npm test", config), "npm test");
|
|
51
|
+
assert.equal(guardTestCommand("npm run build", config), "npm run build");
|
|
52
|
+
assert.equal(guardTestCommand("npm run lint", config), "npm run lint");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("trims leading spaces and matches", () => {
|
|
56
|
+
assert.equal(guardTestCommand(" npm test", config), "npm test");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("trims trailing spaces and matches", () => {
|
|
60
|
+
assert.equal(guardTestCommand("npm test ", config), "npm test");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("trims and matches with double spaces in middle (trim only affects ends)", () => {
|
|
64
|
+
// "npm test" has double space — trim() only trims ends, so "npm test" != "npm test"
|
|
65
|
+
assert.throws(
|
|
66
|
+
() => guardTestCommand("npm test", config),
|
|
67
|
+
PatchWardenError
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects non-allowlisted commands", () => {
|
|
72
|
+
assert.throws(
|
|
73
|
+
() => guardTestCommand("rm -rf /", config),
|
|
74
|
+
PatchWardenError
|
|
75
|
+
);
|
|
76
|
+
assert.throws(
|
|
77
|
+
() => guardTestCommand("curl http://evil.com", config),
|
|
78
|
+
PatchWardenError
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns empty string for empty or undefined command", () => {
|
|
83
|
+
assert.equal(guardTestCommand("", config), "");
|
|
84
|
+
assert.equal(guardTestCommand(undefined as unknown as string, config), "");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns empty string for whitespace-only command", () => {
|
|
88
|
+
assert.equal(guardTestCommand(" ", config), "");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("cannot be bypassed by parameter concatenation", () => {
|
|
92
|
+
// "npm test && rm -rf /" is not in the allowlist
|
|
93
|
+
assert.throws(
|
|
94
|
+
() => guardTestCommand("npm test && rm -rf /", config),
|
|
95
|
+
PatchWardenError
|
|
96
|
+
);
|
|
97
|
+
// "npm test; evil" is not in the allowlist
|
|
98
|
+
assert.throws(
|
|
99
|
+
() => guardTestCommand("npm test; evil", config),
|
|
100
|
+
PatchWardenError
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("guardAgentCommand", () => {
|
|
106
|
+
let tempDir: string;
|
|
107
|
+
let config: PatchWardenConfig;
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-agentguard-"));
|
|
111
|
+
config = makeConfig(tempDir);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("allows configured agents", () => {
|
|
119
|
+
const result = guardAgentCommand("codex", config);
|
|
120
|
+
assert.equal(result.command, "codex");
|
|
121
|
+
assert.deepEqual(result.args, ["exec", "{repo}", "{prompt}"]);
|
|
122
|
+
|
|
123
|
+
const result2 = guardAgentCommand("opencode", config);
|
|
124
|
+
assert.equal(result2.command, "opencode");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects unconfigured agents", () => {
|
|
128
|
+
assert.throws(
|
|
129
|
+
() => guardAgentCommand("evil-agent", config),
|
|
130
|
+
(err: unknown) => err instanceof PatchWardenError && err.reason === "agent_not_configured"
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("rejects empty agent name", () => {
|
|
135
|
+
assert.throws(
|
|
136
|
+
() => guardAgentCommand("", config),
|
|
137
|
+
PatchWardenError
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("guardDirectCommand", () => {
|
|
143
|
+
let tempDir: string;
|
|
144
|
+
let config: PatchWardenConfig;
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-directcmd-"));
|
|
148
|
+
config = makeConfig(tempDir);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("allows commands from direct allowlist", () => {
|
|
156
|
+
assert.equal(guardDirectCommand("npm test", config), "npm test");
|
|
157
|
+
assert.equal(guardDirectCommand("npm run build", config), "npm run build");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("rejects non-allowlisted direct commands", () => {
|
|
161
|
+
assert.throws(
|
|
162
|
+
() => guardDirectCommand("rm -rf /", config),
|
|
163
|
+
PatchWardenError
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("rejects empty command", () => {
|
|
168
|
+
assert.throws(
|
|
169
|
+
() => guardDirectCommand("", config),
|
|
170
|
+
PatchWardenError
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("sanitizePromptArg", () => {
|
|
176
|
+
it("removes null bytes", () => {
|
|
177
|
+
const input = "hello\x00world";
|
|
178
|
+
const result = sanitizePromptArg(input);
|
|
179
|
+
assert.equal(result, "helloworld");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("removes control characters (except tab and newline)", () => {
|
|
183
|
+
const input = "hello\x01\x02\x03world";
|
|
184
|
+
const result = sanitizePromptArg(input);
|
|
185
|
+
assert.equal(result, "helloworld");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("preserves tab characters", () => {
|
|
189
|
+
const input = "hello\tworld";
|
|
190
|
+
const result = sanitizePromptArg(input);
|
|
191
|
+
assert.equal(result, "hello\tworld");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("preserves newline characters", () => {
|
|
195
|
+
const input = "hello\nworld";
|
|
196
|
+
const result = sanitizePromptArg(input);
|
|
197
|
+
assert.equal(result, "hello\nworld");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("preserves carriage return", () => {
|
|
201
|
+
const input = "hello\rworld";
|
|
202
|
+
const result = sanitizePromptArg(input);
|
|
203
|
+
assert.equal(result, "hello\rworld");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("preserves normal text", () => {
|
|
207
|
+
const input = "Fix the bug in main.ts";
|
|
208
|
+
const result = sanitizePromptArg(input);
|
|
209
|
+
assert.equal(result, input);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("removes vertical tab and form feed", () => {
|
|
213
|
+
const input = "hello\x0B\x0Cworld";
|
|
214
|
+
const result = sanitizePromptArg(input);
|
|
215
|
+
assert.equal(result, "helloworld");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("handles empty string", () => {
|
|
219
|
+
assert.equal(sanitizePromptArg(""), "");
|
|
220
|
+
});
|
|
221
|
+
});
|