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,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { resolve, join } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
getDirectSessionsDir,
|
|
11
|
+
getConfig,
|
|
12
|
+
getRepoDirectAllowedCommands,
|
|
13
|
+
PatchWardenConfig,
|
|
14
|
+
} from "../config.js";
|
|
15
|
+
import { guardPath } from "../security/pathGuard.js";
|
|
16
|
+
import { PatchWardenError } from "../errors.js";
|
|
17
|
+
import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
18
|
+
import { getLastToolCatalogSnapshot } from "../tools/toolCatalog.js";
|
|
19
|
+
import {
|
|
20
|
+
captureRepoSnapshot,
|
|
21
|
+
buildChangeArtifacts,
|
|
22
|
+
type RepoSnapshot,
|
|
23
|
+
type ChangeArtifacts,
|
|
24
|
+
} from "../runner/changeCapture.js";
|
|
25
|
+
|
|
26
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface DirectSessionOperation {
|
|
29
|
+
index: number;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
path: string;
|
|
32
|
+
before_sha256: string;
|
|
33
|
+
after_sha256: string;
|
|
34
|
+
operations_applied: number;
|
|
35
|
+
bytes_changed: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DirectSessionVerificationRun {
|
|
39
|
+
command: string;
|
|
40
|
+
exit_code: number | null;
|
|
41
|
+
passed: boolean;
|
|
42
|
+
timed_out: boolean;
|
|
43
|
+
stdout_tail: string;
|
|
44
|
+
stderr_tail: string;
|
|
45
|
+
started_at: string;
|
|
46
|
+
finished_at: string;
|
|
47
|
+
log_path: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DirectSessionRecord {
|
|
51
|
+
session_id: string;
|
|
52
|
+
title: string;
|
|
53
|
+
repo_path: string;
|
|
54
|
+
resolved_repo_path: string;
|
|
55
|
+
created_at: string;
|
|
56
|
+
expires_at: string;
|
|
57
|
+
server_version: string;
|
|
58
|
+
schema_epoch: string;
|
|
59
|
+
tool_manifest_sha256: string;
|
|
60
|
+
workspace_snapshot_before: RepoSnapshot;
|
|
61
|
+
workspace_fingerprint_before: string;
|
|
62
|
+
allowed_commands: string[];
|
|
63
|
+
operations: DirectSessionOperation[];
|
|
64
|
+
verification_runs: DirectSessionVerificationRun[];
|
|
65
|
+
finalized: boolean;
|
|
66
|
+
finalized_at: string | null;
|
|
67
|
+
audited: boolean;
|
|
68
|
+
change_artifacts: ChangeArtifacts | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DirectSessionCreateInput {
|
|
72
|
+
repo_path: string;
|
|
73
|
+
resolved_repo_path: string;
|
|
74
|
+
title?: string;
|
|
75
|
+
snapshot: RepoSnapshot;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── ID generation ──────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export function generateDirectSessionId(): string {
|
|
81
|
+
const timestamp = new Date()
|
|
82
|
+
.toISOString()
|
|
83
|
+
.replace(/[-:]/g, "")
|
|
84
|
+
.replace("T", "_")
|
|
85
|
+
.slice(0, 15);
|
|
86
|
+
const randomHex = randomBytes(16).toString("hex");
|
|
87
|
+
return `direct_${timestamp}_${randomHex}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Directory management ───────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
export function createDirectSessionDir(sessionId: string): string {
|
|
93
|
+
const config = getConfig();
|
|
94
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
95
|
+
guardPath(sessionsDir, config.workspaceRoot, config.directSessionsDir);
|
|
96
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
97
|
+
const dir = resolve(sessionsDir, sessionId);
|
|
98
|
+
guardPath(dir, config.workspaceRoot, config.directSessionsDir);
|
|
99
|
+
mkdirSync(dir, { recursive: true });
|
|
100
|
+
return dir;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getDirectSessionDir(sessionId: string): string {
|
|
104
|
+
const config = getConfig();
|
|
105
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
106
|
+
return resolve(sessionsDir, sessionId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── CRUD ───────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export function createDirectSession(
|
|
112
|
+
input: DirectSessionCreateInput
|
|
113
|
+
): DirectSessionRecord {
|
|
114
|
+
const config = getConfig();
|
|
115
|
+
|
|
116
|
+
const sessionId = generateDirectSessionId();
|
|
117
|
+
const dir = createDirectSessionDir(sessionId);
|
|
118
|
+
|
|
119
|
+
const toolManifest =
|
|
120
|
+
getLastToolCatalogSnapshot()?.tool_manifest_sha256 ||
|
|
121
|
+
computeFallbackManifestHash(config);
|
|
122
|
+
|
|
123
|
+
const workspaceFingerprint = computeWorkspaceFingerprint(input.snapshot);
|
|
124
|
+
|
|
125
|
+
const allowedCommands = [
|
|
126
|
+
...(config.directAllowedCommands || []),
|
|
127
|
+
...getRepoDirectAllowedCommands(config, input.resolved_repo_path),
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const now = new Date();
|
|
131
|
+
const expiresAt = new Date(
|
|
132
|
+
now.getTime() + config.directSessionTtlSeconds * 1000
|
|
133
|
+
).toISOString();
|
|
134
|
+
|
|
135
|
+
const record: DirectSessionRecord = {
|
|
136
|
+
session_id: sessionId,
|
|
137
|
+
title: input.title || "",
|
|
138
|
+
repo_path: input.repo_path,
|
|
139
|
+
resolved_repo_path: input.resolved_repo_path,
|
|
140
|
+
created_at: now.toISOString(),
|
|
141
|
+
expires_at: expiresAt,
|
|
142
|
+
server_version: PATCHWARDEN_VERSION,
|
|
143
|
+
schema_epoch: TOOL_SCHEMA_EPOCH,
|
|
144
|
+
tool_manifest_sha256: toolManifest,
|
|
145
|
+
workspace_snapshot_before: input.snapshot,
|
|
146
|
+
workspace_fingerprint_before: workspaceFingerprint,
|
|
147
|
+
allowed_commands: allowedCommands,
|
|
148
|
+
operations: [],
|
|
149
|
+
verification_runs: [],
|
|
150
|
+
finalized: false,
|
|
151
|
+
finalized_at: null,
|
|
152
|
+
audited: false,
|
|
153
|
+
change_artifacts: null,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join(dir, "session.json"),
|
|
158
|
+
JSON.stringify(record, null, 2),
|
|
159
|
+
"utf-8"
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return record;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function readDirectSession(sessionId: string): DirectSessionRecord {
|
|
166
|
+
const config = getConfig();
|
|
167
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
168
|
+
const dir = resolve(sessionsDir, sessionId);
|
|
169
|
+
const file = join(dir, "session.json");
|
|
170
|
+
guardPath(file, config.workspaceRoot, config.directSessionsDir);
|
|
171
|
+
|
|
172
|
+
if (!existsSync(file)) {
|
|
173
|
+
throw new PatchWardenError(
|
|
174
|
+
"session_not_found",
|
|
175
|
+
`Direct session "${sessionId}" not found.`,
|
|
176
|
+
"Call create_direct_session first to get a valid session_id.",
|
|
177
|
+
true,
|
|
178
|
+
{ session_id: sessionId }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return JSON.parse(readFileSync(file, "utf-8")) as DirectSessionRecord;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function updateDirectSession(
|
|
186
|
+
sessionId: string,
|
|
187
|
+
updates: Partial<DirectSessionRecord>
|
|
188
|
+
): DirectSessionRecord {
|
|
189
|
+
const session = readDirectSession(sessionId);
|
|
190
|
+
const updated: DirectSessionRecord = { ...session, ...updates };
|
|
191
|
+
const dir = getDirectSessionDir(sessionId);
|
|
192
|
+
writeFileSync(
|
|
193
|
+
join(dir, "session.json"),
|
|
194
|
+
JSON.stringify(updated, null, 2),
|
|
195
|
+
"utf-8"
|
|
196
|
+
);
|
|
197
|
+
return updated;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function appendDirectSessionOperation(
|
|
201
|
+
sessionId: string,
|
|
202
|
+
operation: DirectSessionOperation
|
|
203
|
+
): DirectSessionRecord {
|
|
204
|
+
const session = readDirectSession(sessionId);
|
|
205
|
+
const operations = [...session.operations, operation];
|
|
206
|
+
return updateDirectSession(sessionId, { operations });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function appendDirectSessionVerificationRun(
|
|
210
|
+
sessionId: string,
|
|
211
|
+
run: DirectSessionVerificationRun
|
|
212
|
+
): DirectSessionRecord {
|
|
213
|
+
const session = readDirectSession(sessionId);
|
|
214
|
+
const verification_runs = [...session.verification_runs, run];
|
|
215
|
+
return updateDirectSession(sessionId, { verification_runs });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Validation ─────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export interface DirectSessionValidationResult {
|
|
221
|
+
valid: boolean;
|
|
222
|
+
failure_reason: string | null;
|
|
223
|
+
session: DirectSessionRecord | null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function validateDirectSessionFreshness(
|
|
227
|
+
sessionId: string
|
|
228
|
+
): DirectSessionValidationResult {
|
|
229
|
+
let session: DirectSessionRecord;
|
|
230
|
+
try {
|
|
231
|
+
session = readDirectSession(sessionId);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return {
|
|
234
|
+
valid: false,
|
|
235
|
+
failure_reason: extractReason(e),
|
|
236
|
+
session: null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (new Date(session.expires_at) < new Date()) {
|
|
241
|
+
return { valid: false, failure_reason: "session_expired", session };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const currentManifest =
|
|
245
|
+
getLastToolCatalogSnapshot()?.tool_manifest_sha256 ||
|
|
246
|
+
computeFallbackManifestHash(getConfig());
|
|
247
|
+
if (currentManifest !== session.tool_manifest_sha256) {
|
|
248
|
+
return {
|
|
249
|
+
valid: false,
|
|
250
|
+
failure_reason: "session_stale_config",
|
|
251
|
+
session,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { valid: true, failure_reason: null, session };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Finalization ───────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
export function finalizeDirectSessionRecord(
|
|
261
|
+
sessionId: string,
|
|
262
|
+
afterSnapshot: RepoSnapshot,
|
|
263
|
+
changeArtifacts: ChangeArtifacts
|
|
264
|
+
): DirectSessionRecord {
|
|
265
|
+
const dir = getDirectSessionDir(sessionId);
|
|
266
|
+
|
|
267
|
+
// Write change artifacts to session directory
|
|
268
|
+
writeFileSync(
|
|
269
|
+
join(dir, "changed-files.json"),
|
|
270
|
+
JSON.stringify(changeArtifacts.changed_files, null, 2),
|
|
271
|
+
"utf-8"
|
|
272
|
+
);
|
|
273
|
+
writeFileSync(join(dir, "diff.patch"), changeArtifacts.diff, "utf-8");
|
|
274
|
+
|
|
275
|
+
const summary = {
|
|
276
|
+
changed_files_total: changeArtifacts.changed_files.length,
|
|
277
|
+
additions: changeArtifacts.additions,
|
|
278
|
+
deletions: changeArtifacts.deletions,
|
|
279
|
+
patch_mode: changeArtifacts.patch_mode,
|
|
280
|
+
artifact_hygiene: changeArtifacts.artifact_hygiene,
|
|
281
|
+
};
|
|
282
|
+
writeFileSync(
|
|
283
|
+
join(dir, "summary.json"),
|
|
284
|
+
JSON.stringify(summary, null, 2),
|
|
285
|
+
"utf-8"
|
|
286
|
+
);
|
|
287
|
+
writeFileSync(join(dir, "summary.md"), formatSummaryMd(changeArtifacts), "utf-8");
|
|
288
|
+
|
|
289
|
+
return updateDirectSession(sessionId, {
|
|
290
|
+
finalized: true,
|
|
291
|
+
finalized_at: new Date().toISOString(),
|
|
292
|
+
change_artifacts: changeArtifacts,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
export function computeWorkspaceFingerprint(snapshot: RepoSnapshot): string {
|
|
299
|
+
const fileHashes = Object.entries(snapshot.files)
|
|
300
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
301
|
+
.map(([path, fp]) => `${path}:${fp.sha256}`)
|
|
302
|
+
.join("\n");
|
|
303
|
+
return createHash("sha256")
|
|
304
|
+
.update(`${snapshot.head || "null"}\n${snapshot.status}\n${fileHashes}`)
|
|
305
|
+
.digest("hex");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function computeFallbackManifestHash(config: PatchWardenConfig): string {
|
|
309
|
+
return createHash("sha256")
|
|
310
|
+
.update(`${TOOL_SCHEMA_EPOCH}:${config.toolProfile || "full"}`)
|
|
311
|
+
.digest("hex");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function extractReason(error: unknown): string {
|
|
315
|
+
if (error instanceof PatchWardenError) return error.reason;
|
|
316
|
+
if (error instanceof Error) return error.message.slice(0, 80);
|
|
317
|
+
return String(error).slice(0, 80);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatSummaryMd(artifacts: ChangeArtifacts): string {
|
|
321
|
+
const lines: string[] = [
|
|
322
|
+
"# Direct Session Change Summary",
|
|
323
|
+
"",
|
|
324
|
+
`**Changed files:** ${artifacts.changed_files.length}`,
|
|
325
|
+
`**Additions:** ${artifacts.additions}`,
|
|
326
|
+
`**Deletions:** ${artifacts.deletions}`,
|
|
327
|
+
`**Patch mode:** ${artifacts.patch_mode}`,
|
|
328
|
+
"",
|
|
329
|
+
"## Artifact Hygiene",
|
|
330
|
+
"",
|
|
331
|
+
`- Source changes: ${artifacts.artifact_hygiene.counts.source_changes}`,
|
|
332
|
+
`- Tracked build artifacts: ${artifacts.artifact_hygiene.counts.tracked_build_artifacts}`,
|
|
333
|
+
`- Runtime generated files: ${artifacts.artifact_hygiene.counts.runtime_generated_files}`,
|
|
334
|
+
`- Suspicious changes: ${artifacts.artifact_hygiene.counts.suspicious_changes}`,
|
|
335
|
+
"",
|
|
336
|
+
"## Changed Files",
|
|
337
|
+
"",
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
for (const file of artifacts.changed_files) {
|
|
341
|
+
lines.push(`- **${file.change}**: ${file.path}${file.old_path ? ` (from ${file.old_path})` : ""}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return lines.join("\n");
|
|
345
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
import { guardDirectCommand } from "../security/commandGuard.js";
|
|
5
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
6
|
+
import { runSimpleProcess } from "../runner/simpleProcess.js";
|
|
7
|
+
import { getDirectSessionDir } from "./directSessionStore.js";
|
|
8
|
+
import type { DirectSessionVerificationRun } from "./directSessionStore.js";
|
|
9
|
+
|
|
10
|
+
const MAX_TAIL_CHARS = 10_000;
|
|
11
|
+
const DEFAULT_TIMEOUT_SECONDS = 120;
|
|
12
|
+
|
|
13
|
+
export interface RunDirectVerificationInput {
|
|
14
|
+
command: string;
|
|
15
|
+
resolvedRepoPath: string;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
timeoutSeconds?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RunDirectVerificationResult {
|
|
21
|
+
run: DirectSessionVerificationRun;
|
|
22
|
+
log_path: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runDirectVerification(
|
|
26
|
+
input: RunDirectVerificationInput
|
|
27
|
+
): Promise<RunDirectVerificationResult> {
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
const allowedCommand = guardDirectCommand(
|
|
30
|
+
input.command,
|
|
31
|
+
config,
|
|
32
|
+
input.resolvedRepoPath
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const timeoutSeconds = input.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
36
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
37
|
+
|
|
38
|
+
// Parse command into command + args
|
|
39
|
+
const parts = parseCommand(allowedCommand);
|
|
40
|
+
if (!parts) {
|
|
41
|
+
throw new Error(`Failed to parse command: "${allowedCommand}"`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sessionDir = getDirectSessionDir(input.sessionId);
|
|
45
|
+
const logPath = join(sessionDir, "verification.log");
|
|
46
|
+
|
|
47
|
+
const startedAt = new Date().toISOString();
|
|
48
|
+
const result = await runSimpleProcess({
|
|
49
|
+
command: parts.command,
|
|
50
|
+
args: parts.args,
|
|
51
|
+
cwd: input.resolvedRepoPath,
|
|
52
|
+
timeoutMs,
|
|
53
|
+
stdoutPath: logPath,
|
|
54
|
+
stderrPath: logPath,
|
|
55
|
+
});
|
|
56
|
+
const finishedAt = new Date().toISOString();
|
|
57
|
+
|
|
58
|
+
// Redact and truncate output
|
|
59
|
+
const stdoutRedacted = redactSensitiveContent(result.stdout || "");
|
|
60
|
+
const stderrRedacted = redactSensitiveContent(result.stderr || "");
|
|
61
|
+
|
|
62
|
+
const stdoutTail = truncateTail(stdoutRedacted.content, MAX_TAIL_CHARS);
|
|
63
|
+
const stderrTail = truncateTail(stderrRedacted.content, MAX_TAIL_CHARS);
|
|
64
|
+
|
|
65
|
+
const passed = result.exitCode === 0 && !result.timedOut;
|
|
66
|
+
|
|
67
|
+
const run: DirectSessionVerificationRun = {
|
|
68
|
+
command: allowedCommand,
|
|
69
|
+
exit_code: result.exitCode,
|
|
70
|
+
passed,
|
|
71
|
+
timed_out: result.timedOut,
|
|
72
|
+
stdout_tail: stdoutTail,
|
|
73
|
+
stderr_tail: stderrTail,
|
|
74
|
+
started_at: startedAt,
|
|
75
|
+
finished_at: finishedAt,
|
|
76
|
+
log_path: logPath,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Write verification.json
|
|
80
|
+
const verificationJson = {
|
|
81
|
+
command: allowedCommand,
|
|
82
|
+
exit_code: result.exitCode,
|
|
83
|
+
passed,
|
|
84
|
+
timed_out: result.timedOut,
|
|
85
|
+
started_at: startedAt,
|
|
86
|
+
finished_at: finishedAt,
|
|
87
|
+
duration_ms: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
88
|
+
stdout_truncated: result.stdoutTruncated,
|
|
89
|
+
stderr_truncated: result.stderrTruncated,
|
|
90
|
+
redacted: stdoutRedacted.redacted || stderrRedacted.redacted,
|
|
91
|
+
redaction_categories: [
|
|
92
|
+
...new Set([
|
|
93
|
+
...stdoutRedacted.redaction_categories,
|
|
94
|
+
...stderrRedacted.redaction_categories,
|
|
95
|
+
]),
|
|
96
|
+
],
|
|
97
|
+
spawn_error: result.spawnError,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
writeFileSync(
|
|
101
|
+
join(sessionDir, "verification.json"),
|
|
102
|
+
JSON.stringify(verificationJson, null, 2),
|
|
103
|
+
"utf-8"
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return { run, log_path: logPath };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function parseCommand(
|
|
112
|
+
command: string
|
|
113
|
+
): { command: string; args: string[] } | null {
|
|
114
|
+
const trimmed = command.trim();
|
|
115
|
+
if (!trimmed) return null;
|
|
116
|
+
|
|
117
|
+
// Simple split by spaces - sufficient for allowlisted commands like "npm test"
|
|
118
|
+
const parts = trimmed.split(/\s+/);
|
|
119
|
+
|
|
120
|
+
// On Windows, npm/npx are batch files (.cmd) and spawn() cannot find them
|
|
121
|
+
// without shell resolution. Wrap with cmd.exe /c for those commands.
|
|
122
|
+
if (process.platform === "win32" && (parts[0] === "npm" || parts[0] === "npx")) {
|
|
123
|
+
return {
|
|
124
|
+
command: "cmd",
|
|
125
|
+
args: ["/c", ...parts],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
command: parts[0],
|
|
131
|
+
args: parts.slice(1),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function truncateTail(content: string, maxChars: number): string {
|
|
136
|
+
if (content.length <= maxChars) return content;
|
|
137
|
+
return "...(truncated)\n" + content.slice(content.length - maxChars);
|
|
138
|
+
}
|
package/src/doctor.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { isSensitivePath } from "./security/sensitiveGuard.js";
|
|
|
18
18
|
import { guardPlanContent } from "./security/planGuard.js";
|
|
19
19
|
import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
|
|
20
20
|
import { getToolDefs } from "./tools/registry.js";
|
|
21
|
-
import { CHATGPT_CORE_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
21
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
22
22
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
23
23
|
|
|
24
24
|
// ── State ──────────────────────────────────────────────────────────
|
|
@@ -65,6 +65,7 @@ function cmd(cmdStr: string): string {
|
|
|
65
65
|
async function main() {
|
|
66
66
|
|
|
67
67
|
console.log("PatchWarden Doctor\n");
|
|
68
|
+
const allowDefaultConfig = process.argv.includes("--allow-default-config");
|
|
68
69
|
|
|
69
70
|
// 1. Node version
|
|
70
71
|
const nodeVer = process.version;
|
|
@@ -76,6 +77,24 @@ check("Node.js version", nodeMajor >= 18,
|
|
|
76
77
|
const npmVer = cmd("npm --version");
|
|
77
78
|
check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
|
|
78
79
|
|
|
80
|
+
// Local-only medium-risk confirmation entrypoint. It must exist as a package
|
|
81
|
+
// binary but must not be exposed as an MCP tool.
|
|
82
|
+
try {
|
|
83
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
84
|
+
check(
|
|
85
|
+
"patchwarden-confirm package binary",
|
|
86
|
+
packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js",
|
|
87
|
+
packageJson.bin?.["patchwarden-confirm"] || "missing"
|
|
88
|
+
);
|
|
89
|
+
check(
|
|
90
|
+
"patchwarden-confirm compiled entrypoint",
|
|
91
|
+
existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")),
|
|
92
|
+
"run npm.cmd run build"
|
|
93
|
+
);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
check("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error));
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
// 3. Git
|
|
80
99
|
const gitVer = cmd("git --version");
|
|
81
100
|
warnCheck("Git available", gitVer !== "",
|
|
@@ -92,10 +111,14 @@ for (const p of configPaths) {
|
|
|
92
111
|
if (existsSync(p)) { configPathUsed = p; break; }
|
|
93
112
|
}
|
|
94
113
|
|
|
95
|
-
|
|
96
|
-
configPathUsed
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
const configDetail = configPathUsed
|
|
115
|
+
? configPathUsed
|
|
116
|
+
: 'Create one: cp examples/config.example.json patchwarden.config.json';
|
|
117
|
+
if (allowDefaultConfig) {
|
|
118
|
+
warnCheck("Config file exists", configPathUsed !== "", configDetail);
|
|
119
|
+
} else {
|
|
120
|
+
check("Config file exists", configPathUsed !== "", configDetail);
|
|
121
|
+
}
|
|
99
122
|
|
|
100
123
|
// 5. PATCHWARDEN_CONFIG env
|
|
101
124
|
if (process.env.PATCHWARDEN_CONFIG) {
|
|
@@ -219,10 +242,10 @@ const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
|
219
242
|
try {
|
|
220
243
|
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
221
244
|
const fullTools = getToolDefs();
|
|
222
|
-
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core");
|
|
245
|
+
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
|
|
223
246
|
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema as any;
|
|
224
247
|
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema as any;
|
|
225
|
-
check("Full tool profile exposes
|
|
248
|
+
check("Full tool profile exposes 30 tools", fullTools.length === 30, `${fullTools.length} tools`);
|
|
226
249
|
check(
|
|
227
250
|
`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`,
|
|
228
251
|
JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES),
|
|
@@ -237,6 +260,26 @@ try {
|
|
|
237
260
|
waitSchema?.properties?.wait_seconds
|
|
238
261
|
)
|
|
239
262
|
);
|
|
263
|
+
|
|
264
|
+
// chatgpt_direct profile checks
|
|
265
|
+
const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
|
|
266
|
+
check(
|
|
267
|
+
"chatgpt_direct disabled exposes only health_check (degraded mode)",
|
|
268
|
+
directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check",
|
|
269
|
+
`${directDisabledTools.map((t) => t.name).join(", ")}`
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (config?.enableDirectProfile) {
|
|
273
|
+
const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
|
|
274
|
+
check(
|
|
275
|
+
`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`,
|
|
276
|
+
JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES),
|
|
277
|
+
directEnabledTools.map((tool) => tool.name).join(", ")
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
|
|
281
|
+
ok++;
|
|
282
|
+
}
|
|
240
283
|
} finally {
|
|
241
284
|
if (previousProfile === undefined) delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
242
285
|
else process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
|
|
@@ -290,6 +333,33 @@ for (const t of newTools) {
|
|
|
290
333
|
check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
291
334
|
}
|
|
292
335
|
|
|
336
|
+
// Direct tool module checks
|
|
337
|
+
const directToolModules = [
|
|
338
|
+
"createDirectSession",
|
|
339
|
+
"searchWorkspace",
|
|
340
|
+
"applyPatch",
|
|
341
|
+
"runVerification",
|
|
342
|
+
"finalizeDirectSession",
|
|
343
|
+
"auditSession",
|
|
344
|
+
];
|
|
345
|
+
for (const t of directToolModules) {
|
|
346
|
+
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
347
|
+
check(`Direct tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Direct support module checks
|
|
351
|
+
const directSupportModules = [
|
|
352
|
+
"directSessionStore",
|
|
353
|
+
"directGuards",
|
|
354
|
+
"directPatch",
|
|
355
|
+
"directVerification",
|
|
356
|
+
"directAudit",
|
|
357
|
+
];
|
|
358
|
+
for (const t of directSupportModules) {
|
|
359
|
+
const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
|
|
360
|
+
check(`Direct support module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
361
|
+
}
|
|
362
|
+
|
|
293
363
|
// Task directory writable
|
|
294
364
|
if (config) {
|
|
295
365
|
const tasksDir = resolve(config.workspaceRoot, config.tasksDir);
|
|
@@ -380,11 +450,37 @@ if (config) {
|
|
|
380
450
|
}
|
|
381
451
|
}
|
|
382
452
|
|
|
453
|
+
// 14b. Direct profile config checks
|
|
454
|
+
if (config) {
|
|
455
|
+
const directCmds = config.directAllowedCommands || [];
|
|
456
|
+
warnCheck("directAllowedCommands is non-empty", directCmds.length > 0,
|
|
457
|
+
directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured");
|
|
458
|
+
|
|
459
|
+
// npm run doctor should NOT be in default Direct whitelist
|
|
460
|
+
const hasDoctor = directCmds.some((c: string) => c === "npm run doctor");
|
|
461
|
+
check("directAllowedCommands does not include npm run doctor", !hasDoctor,
|
|
462
|
+
hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present");
|
|
463
|
+
|
|
464
|
+
check("directSessionTtlSeconds is valid",
|
|
465
|
+
config.directSessionTtlSeconds >= 60 && config.directSessionTtlSeconds <= 86400,
|
|
466
|
+
`${config.directSessionTtlSeconds}s`);
|
|
467
|
+
|
|
468
|
+
check("directMaxPatchBytes is positive",
|
|
469
|
+
config.directMaxPatchBytes > 0, `${config.directMaxPatchBytes}`);
|
|
470
|
+
|
|
471
|
+
check("directMaxFileBytes is positive",
|
|
472
|
+
config.directMaxFileBytes > 0, `${config.directMaxFileBytes}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
383
475
|
// 15. Tunnel example files check
|
|
384
476
|
const tunnelFiles = [
|
|
385
477
|
"examples/openai-tunnel/README.md",
|
|
386
478
|
"examples/openai-tunnel/tunnel-client.example.yaml",
|
|
387
479
|
"examples/openai-tunnel/chatgpt-test-prompt.md",
|
|
480
|
+
"scripts/patchwarden-mcp-direct.cmd",
|
|
481
|
+
"PatchWarden.cmd",
|
|
482
|
+
"scripts/manage-patchwarden.ps1",
|
|
483
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
388
484
|
];
|
|
389
485
|
for (const tf of tunnelFiles) {
|
|
390
486
|
const full = resolve(process.cwd(), tf);
|