patchwarden 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +29 -0
- package/dist/runner/changeCapture.js +107 -5
- package/dist/runner/runTask.js +70 -44
- 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/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 +94 -2
- 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 +47 -0
- package/dist/tools/getTaskSummary.js +64 -1
- package/dist/tools/healthCheck.d.ts +22 -12
- package/dist/tools/healthCheck.js +23 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +217 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -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/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +3 -2
- package/dist/tools/toolCatalog.js +32 -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/docs/chatgpt-usage.md +101 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -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 +12 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- 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 +66 -6
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +100 -58
- package/scripts/mcp-smoke.js +157 -2
- package/scripts/pack-clean.js +3 -4
- 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/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/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +140 -5
- package/src/runner/runTask.ts +61 -43
- 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/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +100 -2
- 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 +111 -1
- package/src/tools/healthCheck.ts +23 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +242 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +35 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- 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,144 @@
|
|
|
1
|
+
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
readDirectSession,
|
|
4
|
+
updateDirectSession,
|
|
5
|
+
finalizeDirectSessionRecord,
|
|
6
|
+
getDirectSessionDir,
|
|
7
|
+
} from "../direct/directSessionStore.js";
|
|
8
|
+
import { guardDirectSessionActive } from "../direct/directGuards.js";
|
|
9
|
+
import {
|
|
10
|
+
captureRepoSnapshot,
|
|
11
|
+
buildChangeArtifacts,
|
|
12
|
+
type ChangeArtifacts,
|
|
13
|
+
type ClassifiedChange,
|
|
14
|
+
type ChangedFile,
|
|
15
|
+
} from "../runner/changeCapture.js";
|
|
16
|
+
|
|
17
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface FinalizeDirectSessionInput {
|
|
20
|
+
session_id: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FinalizeDirectSessionOutput {
|
|
24
|
+
session_id: string;
|
|
25
|
+
changed_files_total: number;
|
|
26
|
+
source_changes: ClassifiedChange[];
|
|
27
|
+
tracked_build_artifacts: ClassifiedChange[];
|
|
28
|
+
runtime_generated_files: ClassifiedChange[];
|
|
29
|
+
suspicious_changes: ClassifiedChange[];
|
|
30
|
+
out_of_scope_changes: ClassifiedChange[];
|
|
31
|
+
verification_summary: Record<string, "passed" | "failed">;
|
|
32
|
+
diff_path: string;
|
|
33
|
+
summary_path: string;
|
|
34
|
+
finalized: boolean;
|
|
35
|
+
next_action: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Main function ──────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export function finalizeDirectSession(
|
|
41
|
+
input: FinalizeDirectSessionInput
|
|
42
|
+
): FinalizeDirectSessionOutput {
|
|
43
|
+
const { session_id } = input;
|
|
44
|
+
|
|
45
|
+
// 1. Read session and guard active (not expired, not finalized)
|
|
46
|
+
const session = readDirectSession(session_id);
|
|
47
|
+
guardDirectSessionActive(session);
|
|
48
|
+
|
|
49
|
+
// 2. Capture after snapshot
|
|
50
|
+
const afterSnapshot = captureRepoSnapshot(session.resolved_repo_path);
|
|
51
|
+
|
|
52
|
+
// 3. Build change artifacts from before/after snapshots
|
|
53
|
+
const changeArtifacts = buildChangeArtifacts(
|
|
54
|
+
session.resolved_repo_path,
|
|
55
|
+
session.workspace_snapshot_before,
|
|
56
|
+
afterSnapshot
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 4. Finalize session record — writes changed-files.json, diff.patch,
|
|
60
|
+
// summary.json, summary.md and marks the session as finalized.
|
|
61
|
+
finalizeDirectSessionRecord(session_id, afterSnapshot, changeArtifacts);
|
|
62
|
+
|
|
63
|
+
// 5. Build verification summary from session.verification_runs
|
|
64
|
+
const verification_summary: Record<string, "passed" | "failed"> = {};
|
|
65
|
+
for (const run of session.verification_runs) {
|
|
66
|
+
verification_summary[run.command] = run.passed ? "passed" : "failed";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 6. Identify out-of-scope changes (paths outside resolved_repo_path)
|
|
70
|
+
const out_of_scope_changes = findOutOfScopeChanges(
|
|
71
|
+
changeArtifacts,
|
|
72
|
+
session.resolved_repo_path
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// 7. Build compact summary with categorized changes
|
|
76
|
+
const sessionDir = getDirectSessionDir(session_id);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
session_id,
|
|
80
|
+
changed_files_total: changeArtifacts.changed_files.length,
|
|
81
|
+
source_changes: changeArtifacts.artifact_hygiene.source_changes,
|
|
82
|
+
tracked_build_artifacts:
|
|
83
|
+
changeArtifacts.artifact_hygiene.tracked_build_artifacts,
|
|
84
|
+
runtime_generated_files:
|
|
85
|
+
changeArtifacts.artifact_hygiene.runtime_generated_files,
|
|
86
|
+
suspicious_changes: changeArtifacts.artifact_hygiene.suspicious_changes,
|
|
87
|
+
out_of_scope_changes,
|
|
88
|
+
verification_summary,
|
|
89
|
+
diff_path: join(sessionDir, "diff.patch"),
|
|
90
|
+
summary_path: join(sessionDir, "summary.md"),
|
|
91
|
+
finalized: true,
|
|
92
|
+
next_action: "Call audit_session to independently review the changes.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Filter changeArtifacts.changed_files for paths that resolve outside the
|
|
100
|
+
* session's resolved_repo_path and map them to ClassifiedChange format.
|
|
101
|
+
*/
|
|
102
|
+
function findOutOfScopeChanges(
|
|
103
|
+
artifacts: ChangeArtifacts,
|
|
104
|
+
resolvedRepoPath: string
|
|
105
|
+
): ClassifiedChange[] {
|
|
106
|
+
const normalizedRepo = resolve(resolvedRepoPath);
|
|
107
|
+
return artifacts.changed_files
|
|
108
|
+
.filter((file) => {
|
|
109
|
+
const fullPath = resolve(normalizedRepo, file.path);
|
|
110
|
+
const rel = relative(normalizedRepo, fullPath);
|
|
111
|
+
return isAbsolute(rel) || rel.startsWith("..");
|
|
112
|
+
})
|
|
113
|
+
.map((file) => toClassifiedChange(file));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Map a ChangedFile to the ClassifiedChange format by adding a reason string.
|
|
118
|
+
*/
|
|
119
|
+
function toClassifiedChange(file: ChangedFile): ClassifiedChange {
|
|
120
|
+
return {
|
|
121
|
+
path: file.path,
|
|
122
|
+
change: file.change,
|
|
123
|
+
tracked: file.tracked,
|
|
124
|
+
ignored: file.ignored,
|
|
125
|
+
kind: file.kind,
|
|
126
|
+
reason: classificationReason(file),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Produce a human-readable reason for a changed file classification.
|
|
132
|
+
* Mirrors the logic in changeCapture.ts classifyArtifactHygiene.
|
|
133
|
+
*/
|
|
134
|
+
function classificationReason(change: ChangedFile): string {
|
|
135
|
+
if (change.ignored)
|
|
136
|
+
return "untracked path is ignored by repository Git rules";
|
|
137
|
+
if (change.kind === "build_artifact" && change.tracked)
|
|
138
|
+
return "artifact-like path is tracked by Git and requires review";
|
|
139
|
+
if (change.kind === "build_artifact")
|
|
140
|
+
return "artifact-like path is not ignored and requires review";
|
|
141
|
+
if (change.kind === "runtime_generated")
|
|
142
|
+
return "runtime-generated path is not ignored and requires review";
|
|
143
|
+
return change.tracked ? "tracked source change" : "untracked source change";
|
|
144
|
+
}
|
|
@@ -78,9 +78,54 @@ export interface TaskSummaryOutput {
|
|
|
78
78
|
watcher: unknown;
|
|
79
79
|
pending_reason: string | null;
|
|
80
80
|
execution_blocked: boolean;
|
|
81
|
+
artifact_hygiene: Record<string, unknown>;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
export
|
|
84
|
+
export interface CompactTaskSummaryOutput {
|
|
85
|
+
view: "compact";
|
|
86
|
+
task_id: string;
|
|
87
|
+
status: string;
|
|
88
|
+
terminal: boolean;
|
|
89
|
+
acceptance_status: TaskSummaryOutput["acceptance_status"];
|
|
90
|
+
phase: string;
|
|
91
|
+
repo_path: string;
|
|
92
|
+
changed_files_total: number;
|
|
93
|
+
out_of_scope_changes_total: number;
|
|
94
|
+
artifact_hygiene: {
|
|
95
|
+
counts: Record<string, number>;
|
|
96
|
+
source_changes: unknown[];
|
|
97
|
+
tracked_build_artifacts: unknown[];
|
|
98
|
+
ignored_untracked_artifacts: unknown[];
|
|
99
|
+
runtime_generated_files: unknown[];
|
|
100
|
+
suspicious_changes: unknown[];
|
|
101
|
+
max_items: number;
|
|
102
|
+
truncated: boolean;
|
|
103
|
+
};
|
|
104
|
+
verification_summary: TaskSummaryOutput["verification_summary"];
|
|
105
|
+
summary: string;
|
|
106
|
+
warnings: string[];
|
|
107
|
+
errors: string[];
|
|
108
|
+
failure_reason: string | null;
|
|
109
|
+
failed_command: string | null;
|
|
110
|
+
suggested_next_action: string;
|
|
111
|
+
execution_blocked: boolean;
|
|
112
|
+
pending_reason: string | null;
|
|
113
|
+
redacted: boolean;
|
|
114
|
+
redaction_categories: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface GetTaskSummaryOptions {
|
|
118
|
+
view?: "compact" | "standard";
|
|
119
|
+
max_items?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type TaskSummaryResult = TaskSummaryOutput | CompactTaskSummaryOutput;
|
|
123
|
+
|
|
124
|
+
export function getTaskSummary(taskId: string): TaskSummaryOutput;
|
|
125
|
+
export function getTaskSummary(taskId: string, options: { view: "compact"; max_items?: number }): CompactTaskSummaryOutput;
|
|
126
|
+
export function getTaskSummary(taskId: string, options: { view?: "standard"; max_items?: number }): TaskSummaryOutput;
|
|
127
|
+
export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions): TaskSummaryResult;
|
|
128
|
+
export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions = {}): TaskSummaryResult {
|
|
84
129
|
const config = getConfig();
|
|
85
130
|
const taskDir = resolve(getTasksDir(config), taskId);
|
|
86
131
|
const statusFile = join(taskDir, "status.json");
|
|
@@ -156,6 +201,10 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
|
|
|
156
201
|
? Math.max(0, (Number.isFinite(finishedAt) ? finishedAt : Date.now()) - startedAt)
|
|
157
202
|
: 0;
|
|
158
203
|
const changedFiles = asArray(result.changed_files ?? status.changed_files);
|
|
204
|
+
const changedFilesRead = tryReadJson(join(taskDir, "changed-files.json"));
|
|
205
|
+
const artifactHygiene = asRecord(result.artifact_hygiene ?? changedFilesRead.data.artifact_hygiene ?? {
|
|
206
|
+
counts: status.artifact_hygiene_counts || {},
|
|
207
|
+
});
|
|
159
208
|
const verifyCommands = asArray(verify.commands ?? result.verify_commands ?? result.verify?.commands);
|
|
160
209
|
const testLogSummary = summarizeTestLog(join(taskDir, "test.log"));
|
|
161
210
|
const verificationSummary = buildVerificationSummary(verifyStatus, verifyCommands, testLogSummary);
|
|
@@ -227,7 +276,11 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
|
|
|
227
276
|
watcher: status.watcher,
|
|
228
277
|
pending_reason: status.pending_reason || null,
|
|
229
278
|
execution_blocked: Boolean(status.execution_blocked),
|
|
279
|
+
artifact_hygiene: artifactHygiene,
|
|
230
280
|
};
|
|
281
|
+
if ((options.view || "standard") === "compact") {
|
|
282
|
+
return buildCompactSummary(output, normalizeMaxItems(options.max_items));
|
|
283
|
+
}
|
|
231
284
|
const safe = redactSensitiveValue(output);
|
|
232
285
|
return {
|
|
233
286
|
...safe.value,
|
|
@@ -236,6 +289,59 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
|
|
|
236
289
|
} as TaskSummaryOutput;
|
|
237
290
|
}
|
|
238
291
|
|
|
292
|
+
function buildCompactSummary(output: Record<string, any>, maxItems: number): CompactTaskSummaryOutput {
|
|
293
|
+
const hygiene = asRecord(output.artifact_hygiene);
|
|
294
|
+
const groupNames = [
|
|
295
|
+
"source_changes",
|
|
296
|
+
"tracked_build_artifacts",
|
|
297
|
+
"ignored_untracked_artifacts",
|
|
298
|
+
"runtime_generated_files",
|
|
299
|
+
"suspicious_changes",
|
|
300
|
+
] as const;
|
|
301
|
+
const groups = Object.fromEntries(groupNames.map((name) => [name, asArray(hygiene[name]).slice(0, maxItems)]));
|
|
302
|
+
const truncated = groupNames.some((name) => asArray(hygiene[name]).length > maxItems);
|
|
303
|
+
const compact = {
|
|
304
|
+
view: "compact" as const,
|
|
305
|
+
task_id: String(output.task_id),
|
|
306
|
+
status: String(output.status),
|
|
307
|
+
terminal: Boolean(output.terminal),
|
|
308
|
+
acceptance_status: output.acceptance_status,
|
|
309
|
+
phase: String(output.phase),
|
|
310
|
+
repo_path: String(output.repo_path),
|
|
311
|
+
changed_files_total: asArray(output.changed_files).length,
|
|
312
|
+
out_of_scope_changes_total: asArray(output.out_of_scope_changes).length,
|
|
313
|
+
artifact_hygiene: {
|
|
314
|
+
counts: asRecord(hygiene.counts) as Record<string, number>,
|
|
315
|
+
...groups,
|
|
316
|
+
max_items: maxItems,
|
|
317
|
+
truncated,
|
|
318
|
+
},
|
|
319
|
+
verification_summary: output.verification_summary,
|
|
320
|
+
summary: String(output.summary).slice(0, 1000),
|
|
321
|
+
warnings: asArray(output.warnings).slice(0, maxItems),
|
|
322
|
+
errors: asArray(output.errors).slice(0, maxItems),
|
|
323
|
+
failure_reason: output.failure_reason || null,
|
|
324
|
+
failed_command: output.failed_command || null,
|
|
325
|
+
suggested_next_action: String(output.suggested_next_action),
|
|
326
|
+
execution_blocked: Boolean(output.execution_blocked),
|
|
327
|
+
pending_reason: output.pending_reason || null,
|
|
328
|
+
};
|
|
329
|
+
const safe = redactSensitiveValue(compact);
|
|
330
|
+
return {
|
|
331
|
+
...safe.value,
|
|
332
|
+
redacted: safe.redacted,
|
|
333
|
+
redaction_categories: safe.redaction_categories,
|
|
334
|
+
} as CompactTaskSummaryOutput;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function normalizeMaxItems(value: number | undefined): number {
|
|
338
|
+
if (value === undefined) return 8;
|
|
339
|
+
if (!Number.isInteger(value) || value < 1 || value > 50) {
|
|
340
|
+
throw new Error("max_items must be an integer from 1 to 50.");
|
|
341
|
+
}
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
|
|
239
345
|
function tryReadJson(path: string): { data: Record<string, any>; error?: string } {
|
|
240
346
|
if (!existsSync(path)) return { data: {} };
|
|
241
347
|
try {
|
|
@@ -249,6 +355,10 @@ function asArray(value: unknown): any[] {
|
|
|
249
355
|
return Array.isArray(value) ? value : [];
|
|
250
356
|
}
|
|
251
357
|
|
|
358
|
+
function asRecord(value: unknown): Record<string, any> {
|
|
359
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
|
|
360
|
+
}
|
|
361
|
+
|
|
252
362
|
function summarizeTestLog(path: string): string {
|
|
253
363
|
if (!existsSync(path)) return "test.log missing";
|
|
254
364
|
const text = readFileSync(path, "utf-8");
|
package/src/tools/healthCheck.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { getConfig, getTasksDir, resolveWorkspaceRoot } from "../config.js";
|
|
4
|
+
import { getConfig, getTasksDir, getDirectSessionsDir, resolveWorkspaceRoot } from "../config.js";
|
|
5
5
|
import { listAgents } from "./listAgents.js";
|
|
6
6
|
import { redactSensitiveContent, redactSensitiveValue } from "../security/contentRedaction.js";
|
|
7
7
|
import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
8
8
|
import type { ToolCatalogSnapshot } from "./toolCatalog.js";
|
|
9
|
-
import { CHATGPT_CORE_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
9
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
10
10
|
import { readWatcherStatus } from "../watcherStatus.js";
|
|
11
11
|
import { listTasks } from "./listTasks.js";
|
|
12
12
|
|
|
@@ -35,9 +35,17 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
35
35
|
let profileConsistent = true;
|
|
36
36
|
if (catalog) {
|
|
37
37
|
const activeProfile = resolveToolProfile(config.toolProfile);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
let expectedNames: string[] | null = null;
|
|
39
|
+
if (activeProfile === "chatgpt_core") {
|
|
40
|
+
expectedNames = [...CHATGPT_CORE_TOOL_NAMES];
|
|
41
|
+
} else if (activeProfile === "chatgpt_direct" && config.enableDirectProfile) {
|
|
42
|
+
expectedNames = [...CHATGPT_DIRECT_TOOL_NAMES];
|
|
43
|
+
} else if (activeProfile === "chatgpt_direct" && !config.enableDirectProfile) {
|
|
44
|
+
// Degraded mode: only health_check should be exposed
|
|
45
|
+
expectedNames = ["health_check"];
|
|
46
|
+
profileErrors.push("Direct profile is disabled (enableDirectProfile=false). Only health_check is available. Set enableDirectProfile: true to enable Direct session tools.");
|
|
47
|
+
profileConsistent = false;
|
|
48
|
+
}
|
|
41
49
|
if (expectedNames) {
|
|
42
50
|
const catalogNames = new Set(catalog.tool_names);
|
|
43
51
|
for (const name of expectedNames) {
|
|
@@ -76,6 +84,11 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
76
84
|
const status = watcher.available && workspace.available && tasks.available && agentReady && catalogConsistent
|
|
77
85
|
? "healthy"
|
|
78
86
|
: "degraded";
|
|
87
|
+
|
|
88
|
+
const directSessionsDir = getDirectSessionsDir(config);
|
|
89
|
+
const directSessions = directoryStatus(directSessionsDir, true, workspaceRoot);
|
|
90
|
+
const activeProfile = resolveToolProfile(config.toolProfile);
|
|
91
|
+
|
|
79
92
|
return {
|
|
80
93
|
status,
|
|
81
94
|
server_version: catalog?.server_version || PATCHWARDEN_VERSION,
|
|
@@ -89,11 +102,15 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
89
102
|
catalog_consistent: catalogConsistent,
|
|
90
103
|
mismatch_report: mismatchReport,
|
|
91
104
|
tunnel_catalog_comparison: tunnelCatalogComparison,
|
|
105
|
+
direct_profile_enabled: config.enableDirectProfile ?? false,
|
|
106
|
+
direct_sessions_dir: directSessions,
|
|
107
|
+
direct_session_ttl_seconds: config.directSessionTtlSeconds,
|
|
108
|
+
...(activeProfile === "chatgpt_direct" ? { direct_tool_count: config.enableDirectProfile ? CHATGPT_DIRECT_TOOL_NAMES.length : 1 } : {}),
|
|
92
109
|
connector_visibility: {
|
|
93
110
|
status: "not_observable_server_side",
|
|
94
111
|
verification: "Refresh or reconnect the Connector and verify tools/list from a new ChatGPT conversation.",
|
|
95
112
|
refresh_steps: [
|
|
96
|
-
"1. Run
|
|
113
|
+
"1. Run PatchWarden.cmd health to confirm the active profile, tool count, and catalog consistency.",
|
|
97
114
|
"2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
|
|
98
115
|
"3. Open a NEW ChatGPT conversation (old conversations retain their cached tool catalog).",
|
|
99
116
|
"4. Call health_check in the new conversation; verify tool_manifest_sha256 matches the local report.",
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { resolve, relative, isAbsolute } from "node:path";
|
|
2
4
|
import { getConfig } from "../config.js";
|
|
3
5
|
import { guardReadPath } from "../security/pathGuard.js";
|
|
4
6
|
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
5
7
|
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
8
|
+
import { PatchWardenError } from "../errors.js";
|
|
9
|
+
import { readDirectSession } from "../direct/directSessionStore.js";
|
|
10
|
+
import { guardDirectSessionActive, guardDirectReadPath, isBinaryFile, guardDirectFileSize } from "../direct/directGuards.js";
|
|
11
|
+
import { resolveToolProfile } from "./toolCatalog.js";
|
|
6
12
|
|
|
7
13
|
export interface ReadWorkspaceFileOutput {
|
|
8
14
|
path: string;
|
|
@@ -10,11 +16,41 @@ export interface ReadWorkspaceFileOutput {
|
|
|
10
16
|
size: number;
|
|
11
17
|
redacted?: boolean;
|
|
12
18
|
redaction_categories?: string[];
|
|
19
|
+
// Direct mode extensions
|
|
20
|
+
relative_path?: string;
|
|
21
|
+
sha256?: string;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
export
|
|
24
|
+
export interface ReadWorkspaceFileInput {
|
|
25
|
+
path: string;
|
|
26
|
+
session_id?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readWorkspaceFile(input: string | ReadWorkspaceFileInput): ReadWorkspaceFileOutput {
|
|
16
30
|
const config = getConfig();
|
|
17
31
|
|
|
32
|
+
// Support both old string API and new object API
|
|
33
|
+
const relativePath = typeof input === "string" ? input : input.path;
|
|
34
|
+
const sessionId = typeof input === "string" ? undefined : input.session_id;
|
|
35
|
+
|
|
36
|
+
// Direct mode: session_id provided
|
|
37
|
+
if (sessionId) {
|
|
38
|
+
return readWorkspaceFileDirect(relativePath, sessionId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// In chatgpt_direct profile, session_id is required
|
|
42
|
+
const profile = resolveToolProfile(config.toolProfile);
|
|
43
|
+
if (profile === "chatgpt_direct") {
|
|
44
|
+
throw new PatchWardenError(
|
|
45
|
+
"session_id_required",
|
|
46
|
+
"session_id is required when using the chatgpt_direct profile.",
|
|
47
|
+
"Provide a session_id from create_direct_session.",
|
|
48
|
+
true,
|
|
49
|
+
{ operation: "read_workspace_file" }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Compatibility mode: no session_id, original behavior
|
|
18
54
|
const safePath = guardReadPath(relativePath, config.workspaceRoot);
|
|
19
55
|
guardSensitivePath(safePath);
|
|
20
56
|
|
|
@@ -39,3 +75,50 @@ export function readWorkspaceFile(relativePath: string): ReadWorkspaceFileOutput
|
|
|
39
75
|
redaction_categories: redaction.redaction_categories,
|
|
40
76
|
};
|
|
41
77
|
}
|
|
78
|
+
|
|
79
|
+
function readWorkspaceFileDirect(relativePath: string, sessionId: string): ReadWorkspaceFileOutput {
|
|
80
|
+
const config = getConfig();
|
|
81
|
+
const session = readDirectSession(sessionId);
|
|
82
|
+
guardDirectSessionActive(session);
|
|
83
|
+
|
|
84
|
+
// Resolve path within session's repo
|
|
85
|
+
const safePath = guardDirectReadPath(relativePath, session.resolved_repo_path, config.workspaceRoot);
|
|
86
|
+
|
|
87
|
+
if (!existsSync(safePath)) {
|
|
88
|
+
throw new PatchWardenError(
|
|
89
|
+
"file_not_found",
|
|
90
|
+
`File "${relativePath}" not found in session repo.`,
|
|
91
|
+
"Use search_workspace to find available files.",
|
|
92
|
+
true,
|
|
93
|
+
{ path: relativePath, session_id: sessionId }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const stat = statSync(safePath);
|
|
98
|
+
guardDirectFileSize(stat.size);
|
|
99
|
+
|
|
100
|
+
// Block binary files
|
|
101
|
+
if (isBinaryFile(safePath)) {
|
|
102
|
+
throw new PatchWardenError(
|
|
103
|
+
"binary_file_blocked",
|
|
104
|
+
`File "${relativePath}" appears to be a binary file.`,
|
|
105
|
+
"Binary files cannot be read in Direct mode.",
|
|
106
|
+
true,
|
|
107
|
+
{ path: relativePath }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const content = readFileSync(safePath, "utf-8");
|
|
112
|
+
const redaction = redactSensitiveContent(content);
|
|
113
|
+
const sha256 = createHash("sha256").update(content, "utf-8").digest("hex");
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
path: safePath,
|
|
117
|
+
relative_path: relativePath,
|
|
118
|
+
content: redaction.content,
|
|
119
|
+
size: stat.size,
|
|
120
|
+
sha256,
|
|
121
|
+
redacted: redaction.redacted,
|
|
122
|
+
redaction_categories: redaction.redaction_categories,
|
|
123
|
+
};
|
|
124
|
+
}
|