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
|
@@ -12,7 +12,7 @@ const TERMINAL_STATUSES = new Set([
|
|
|
12
12
|
"failed_policy_violation",
|
|
13
13
|
"canceled",
|
|
14
14
|
]);
|
|
15
|
-
export function getTaskSummary(taskId) {
|
|
15
|
+
export function getTaskSummary(taskId, options = {}) {
|
|
16
16
|
const config = getConfig();
|
|
17
17
|
const taskDir = resolve(getTasksDir(config), taskId);
|
|
18
18
|
const statusFile = join(taskDir, "status.json");
|
|
@@ -23,7 +23,12 @@ export function getTaskSummary(taskId) {
|
|
|
23
23
|
const result = resultRead.data;
|
|
24
24
|
const verify = verifyRead.data;
|
|
25
25
|
const terminal = TERMINAL_STATUSES.has(String(status.status));
|
|
26
|
-
|
|
26
|
+
// Phase 4: Use new_out_of_scope_changes (task-caused) for acceptance status.
|
|
27
|
+
// Pre-existing external dirty files that didn't change should NOT fail acceptance.
|
|
28
|
+
const outOfScope = asArray(result.new_out_of_scope_changes
|
|
29
|
+
?? status.new_out_of_scope_changes
|
|
30
|
+
?? result.out_of_scope_changes
|
|
31
|
+
?? status.out_of_scope_changes);
|
|
27
32
|
const verifyStatus = String(verify.status ?? result.verify_status ?? result.verify?.status ?? status.verify_status ?? "not_available");
|
|
28
33
|
const errors = [status.error, ...asArray(result.errors), ...asArray(result.known_issues)]
|
|
29
34
|
.filter((value) => typeof value === "string" && value.trim() !== "");
|
|
@@ -92,6 +97,10 @@ export function getTaskSummary(taskId) {
|
|
|
92
97
|
? Math.max(0, (Number.isFinite(finishedAt) ? finishedAt : Date.now()) - startedAt)
|
|
93
98
|
: 0;
|
|
94
99
|
const changedFiles = asArray(result.changed_files ?? status.changed_files);
|
|
100
|
+
const changedFilesRead = tryReadJson(join(taskDir, "changed-files.json"));
|
|
101
|
+
const artifactHygiene = asRecord(result.artifact_hygiene ?? changedFilesRead.data.artifact_hygiene ?? {
|
|
102
|
+
counts: status.artifact_hygiene_counts || {},
|
|
103
|
+
});
|
|
95
104
|
const verifyCommands = asArray(verify.commands ?? result.verify_commands ?? result.verify?.commands);
|
|
96
105
|
const testLogSummary = summarizeTestLog(join(taskDir, "test.log"));
|
|
97
106
|
const verificationSummary = buildVerificationSummary(verifyStatus, verifyCommands, testLogSummary);
|
|
@@ -156,7 +165,11 @@ export function getTaskSummary(taskId) {
|
|
|
156
165
|
watcher: status.watcher,
|
|
157
166
|
pending_reason: status.pending_reason || null,
|
|
158
167
|
execution_blocked: Boolean(status.execution_blocked),
|
|
168
|
+
artifact_hygiene: artifactHygiene,
|
|
159
169
|
};
|
|
170
|
+
if ((options.view || "standard") === "compact") {
|
|
171
|
+
return buildCompactSummary(output, normalizeMaxItems(options.max_items));
|
|
172
|
+
}
|
|
160
173
|
const safe = redactSensitiveValue(output);
|
|
161
174
|
return {
|
|
162
175
|
...safe.value,
|
|
@@ -164,6 +177,67 @@ export function getTaskSummary(taskId) {
|
|
|
164
177
|
redaction_categories: safe.redaction_categories,
|
|
165
178
|
};
|
|
166
179
|
}
|
|
180
|
+
function buildCompactSummary(output, maxItems) {
|
|
181
|
+
const hygiene = asRecord(output.artifact_hygiene);
|
|
182
|
+
const groupNames = [
|
|
183
|
+
"source_changes",
|
|
184
|
+
"tracked_build_artifacts",
|
|
185
|
+
"ignored_untracked_artifacts",
|
|
186
|
+
"runtime_generated_files",
|
|
187
|
+
"suspicious_changes",
|
|
188
|
+
];
|
|
189
|
+
const groups = Object.fromEntries(groupNames.map((name) => [name, asArray(hygiene[name]).slice(0, maxItems)]));
|
|
190
|
+
const truncated = groupNames.some((name) => asArray(hygiene[name]).length > maxItems);
|
|
191
|
+
// Phase 6: Read artifact_manifest.json for release artifact info
|
|
192
|
+
const taskDir = resolve(getTasksDir(getConfig()), String(output.task_id));
|
|
193
|
+
const manifestRead = tryReadJson(join(taskDir, "artifact_manifest.json"));
|
|
194
|
+
const manifest = asRecord(manifestRead.data);
|
|
195
|
+
const releaseArtifacts = asArray(manifest.artifacts);
|
|
196
|
+
const releaseArtifactPaths = releaseArtifacts.map((a) => String(a.path || "")).slice(0, maxItems);
|
|
197
|
+
const compact = {
|
|
198
|
+
view: "compact",
|
|
199
|
+
task_id: String(output.task_id),
|
|
200
|
+
status: String(output.status),
|
|
201
|
+
terminal: Boolean(output.terminal),
|
|
202
|
+
acceptance_status: output.acceptance_status,
|
|
203
|
+
phase: String(output.phase),
|
|
204
|
+
repo_path: String(output.repo_path),
|
|
205
|
+
changed_files_total: asArray(output.changed_files).length,
|
|
206
|
+
out_of_scope_changes_total: asArray(output.out_of_scope_changes).length,
|
|
207
|
+
artifact_hygiene: {
|
|
208
|
+
counts: asRecord(hygiene.counts),
|
|
209
|
+
...groups,
|
|
210
|
+
max_items: maxItems,
|
|
211
|
+
truncated,
|
|
212
|
+
},
|
|
213
|
+
release_artifacts_count: releaseArtifacts.length,
|
|
214
|
+
release_artifact_paths: releaseArtifactPaths,
|
|
215
|
+
artifact_status: String(output.artifact_status || manifest.status || "collected"),
|
|
216
|
+
verification_summary: output.verification_summary,
|
|
217
|
+
summary: String(output.summary).slice(0, 1000),
|
|
218
|
+
warnings: asArray(output.warnings).slice(0, maxItems),
|
|
219
|
+
errors: asArray(output.errors).slice(0, maxItems),
|
|
220
|
+
failure_reason: output.failure_reason || null,
|
|
221
|
+
failed_command: output.failed_command || null,
|
|
222
|
+
suggested_next_action: String(output.suggested_next_action),
|
|
223
|
+
execution_blocked: Boolean(output.execution_blocked),
|
|
224
|
+
pending_reason: output.pending_reason || null,
|
|
225
|
+
};
|
|
226
|
+
const safe = redactSensitiveValue(compact);
|
|
227
|
+
return {
|
|
228
|
+
...safe.value,
|
|
229
|
+
redacted: safe.redacted,
|
|
230
|
+
redaction_categories: safe.redaction_categories,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function normalizeMaxItems(value) {
|
|
234
|
+
if (value === undefined)
|
|
235
|
+
return 8;
|
|
236
|
+
if (!Number.isInteger(value) || value < 1 || value > 50) {
|
|
237
|
+
throw new Error("max_items must be an integer from 1 to 50.");
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
167
241
|
function tryReadJson(path) {
|
|
168
242
|
if (!existsSync(path))
|
|
169
243
|
return { data: {} };
|
|
@@ -177,6 +251,9 @@ function tryReadJson(path) {
|
|
|
177
251
|
function asArray(value) {
|
|
178
252
|
return Array.isArray(value) ? value : [];
|
|
179
253
|
}
|
|
254
|
+
function asRecord(value) {
|
|
255
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
256
|
+
}
|
|
180
257
|
function summarizeTestLog(path) {
|
|
181
258
|
if (!existsSync(path))
|
|
182
259
|
return "test.log missing";
|
|
@@ -20,18 +20,6 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
|
|
|
20
20
|
error: string | null;
|
|
21
21
|
}[];
|
|
22
22
|
} | undefined;
|
|
23
|
-
status: string;
|
|
24
|
-
server_version: string;
|
|
25
|
-
schema_epoch: string;
|
|
26
|
-
tool_profile: string;
|
|
27
|
-
tool_count: number | null;
|
|
28
|
-
tool_names: string[];
|
|
29
|
-
tool_manifest_sha256: string | null;
|
|
30
|
-
profile_consistent: boolean;
|
|
31
|
-
profile_errors: string[];
|
|
32
|
-
catalog_consistent: boolean;
|
|
33
|
-
mismatch_report: string[];
|
|
34
|
-
tunnel_catalog_comparison: string;
|
|
35
23
|
connector_visibility: {
|
|
36
24
|
status: string;
|
|
37
25
|
verification: string;
|
|
@@ -43,6 +31,10 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
|
|
|
43
31
|
uptime_seconds: number;
|
|
44
32
|
checked_at: string;
|
|
45
33
|
};
|
|
34
|
+
path_encoding: {
|
|
35
|
+
encoding: string;
|
|
36
|
+
warnings: string[];
|
|
37
|
+
};
|
|
46
38
|
watcher: {
|
|
47
39
|
supervisor: Record<string, unknown>;
|
|
48
40
|
status: import("../watcherStatus.js").WatcherState;
|
|
@@ -54,6 +46,7 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
|
|
|
54
46
|
instance_id: string | null;
|
|
55
47
|
launcher_pid: number | null;
|
|
56
48
|
reason: string | null;
|
|
49
|
+
activity: string | null;
|
|
57
50
|
};
|
|
58
51
|
workspace_root: {
|
|
59
52
|
available: boolean;
|
|
@@ -76,4 +69,26 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
|
|
|
76
69
|
agent_status: {
|
|
77
70
|
[k: string]: string;
|
|
78
71
|
};
|
|
72
|
+
direct_tool_count?: number | undefined;
|
|
73
|
+
status: string;
|
|
74
|
+
server_version: string;
|
|
75
|
+
schema_epoch: string;
|
|
76
|
+
tool_profile: string;
|
|
77
|
+
tool_count: number | null;
|
|
78
|
+
tool_names: string[];
|
|
79
|
+
tool_manifest_sha256: string | null;
|
|
80
|
+
profile_consistent: boolean;
|
|
81
|
+
profile_errors: string[];
|
|
82
|
+
catalog_consistent: boolean;
|
|
83
|
+
mismatch_report: string[];
|
|
84
|
+
tunnel_catalog_comparison: string;
|
|
85
|
+
direct_profile_enabled: boolean;
|
|
86
|
+
direct_sessions_dir: {
|
|
87
|
+
available: boolean;
|
|
88
|
+
path: string;
|
|
89
|
+
readable: boolean;
|
|
90
|
+
writable: boolean;
|
|
91
|
+
reason: string | null;
|
|
92
|
+
};
|
|
93
|
+
direct_session_ttl_seconds: number;
|
|
79
94
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
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
|
-
import { CHATGPT_CORE_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
8
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
9
9
|
import { readWatcherStatus } from "../watcherStatus.js";
|
|
10
10
|
import { listTasks } from "./listTasks.js";
|
|
11
11
|
const SERVER_STARTED_AT = Date.now();
|
|
@@ -26,9 +26,19 @@ export function healthCheck(catalog, input = {}) {
|
|
|
26
26
|
let profileConsistent = true;
|
|
27
27
|
if (catalog) {
|
|
28
28
|
const activeProfile = resolveToolProfile(config.toolProfile);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
let expectedNames = null;
|
|
30
|
+
if (activeProfile === "chatgpt_core") {
|
|
31
|
+
expectedNames = [...CHATGPT_CORE_TOOL_NAMES];
|
|
32
|
+
}
|
|
33
|
+
else if (activeProfile === "chatgpt_direct" && config.enableDirectProfile) {
|
|
34
|
+
expectedNames = [...CHATGPT_DIRECT_TOOL_NAMES];
|
|
35
|
+
}
|
|
36
|
+
else if (activeProfile === "chatgpt_direct" && !config.enableDirectProfile) {
|
|
37
|
+
// Degraded mode: only health_check should be exposed
|
|
38
|
+
expectedNames = ["health_check"];
|
|
39
|
+
profileErrors.push("Direct profile is disabled (enableDirectProfile=false). Only health_check is available. Set enableDirectProfile: true to enable Direct session tools.");
|
|
40
|
+
profileConsistent = false;
|
|
41
|
+
}
|
|
32
42
|
if (expectedNames) {
|
|
33
43
|
const catalogNames = new Set(catalog.tool_names);
|
|
34
44
|
for (const name of expectedNames) {
|
|
@@ -65,6 +75,9 @@ export function healthCheck(catalog, input = {}) {
|
|
|
65
75
|
const status = watcher.available && workspace.available && tasks.available && agentReady && catalogConsistent
|
|
66
76
|
? "healthy"
|
|
67
77
|
: "degraded";
|
|
78
|
+
const directSessionsDir = getDirectSessionsDir(config);
|
|
79
|
+
const directSessions = directoryStatus(directSessionsDir, true, workspaceRoot);
|
|
80
|
+
const activeProfile = resolveToolProfile(config.toolProfile);
|
|
68
81
|
return {
|
|
69
82
|
status,
|
|
70
83
|
server_version: catalog?.server_version || PATCHWARDEN_VERSION,
|
|
@@ -78,11 +91,15 @@ export function healthCheck(catalog, input = {}) {
|
|
|
78
91
|
catalog_consistent: catalogConsistent,
|
|
79
92
|
mismatch_report: mismatchReport,
|
|
80
93
|
tunnel_catalog_comparison: tunnelCatalogComparison,
|
|
94
|
+
direct_profile_enabled: config.enableDirectProfile ?? false,
|
|
95
|
+
direct_sessions_dir: directSessions,
|
|
96
|
+
direct_session_ttl_seconds: config.directSessionTtlSeconds,
|
|
97
|
+
...(activeProfile === "chatgpt_direct" ? { direct_tool_count: config.enableDirectProfile ? CHATGPT_DIRECT_TOOL_NAMES.length : 1 } : {}),
|
|
81
98
|
connector_visibility: {
|
|
82
99
|
status: "not_observable_server_side",
|
|
83
100
|
verification: "Refresh or reconnect the Connector and verify tools/list from a new ChatGPT conversation.",
|
|
84
101
|
refresh_steps: [
|
|
85
|
-
"1. Run
|
|
102
|
+
"1. Run PatchWarden.cmd health to confirm the active profile, tool count, and catalog consistency.",
|
|
86
103
|
"2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
|
|
87
104
|
"3. Open a NEW ChatGPT conversation (old conversations retain their cached tool catalog).",
|
|
88
105
|
"4. Call health_check in the new conversation; verify tool_manifest_sha256 matches the local report.",
|
|
@@ -94,6 +111,7 @@ export function healthCheck(catalog, input = {}) {
|
|
|
94
111
|
uptime_seconds: Math.round((Date.now() - SERVER_STARTED_AT) / 1000),
|
|
95
112
|
checked_at: new Date().toISOString(),
|
|
96
113
|
},
|
|
114
|
+
path_encoding: checkPathEncoding(),
|
|
97
115
|
watcher,
|
|
98
116
|
workspace_root: workspace,
|
|
99
117
|
tasks_dir: tasks,
|
|
@@ -228,3 +246,23 @@ function readTunnelStatus() {
|
|
|
228
246
|
function safeText(value, maxLength) {
|
|
229
247
|
return typeof value === "string" ? value.replace(/[\r\n]+/g, " ").slice(0, maxLength) : "";
|
|
230
248
|
}
|
|
249
|
+
function checkPathEncoding() {
|
|
250
|
+
const warnings = [];
|
|
251
|
+
// Node.js always uses UTF-8 for fs operations, but on Windows the console
|
|
252
|
+
// codepage can cause display issues. We check if the environment is likely
|
|
253
|
+
// to cause problems with non-ASCII paths.
|
|
254
|
+
if (process.platform === "win32") {
|
|
255
|
+
// Check if the system locale uses a codepage that might not support UTF-8
|
|
256
|
+
const lang = process.env.LANG || process.env.LC_ALL || "";
|
|
257
|
+
const chcp = process.env.PATCHWARDEN_CHCP || "";
|
|
258
|
+
if (chcp && !chcp.includes("65001") && !chcp.includes("utf")) {
|
|
259
|
+
warnings.push(`Windows codepage "${chcp}" may cause display issues with non-ASCII paths. Consider setting codepage to 65001 (UTF-8).`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Verify that Node.js fs operations use UTF-8 by default
|
|
263
|
+
// This is always true in Node.js >= 18
|
|
264
|
+
return {
|
|
265
|
+
encoding: "utf-8",
|
|
266
|
+
warnings,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -4,5 +4,11 @@ export interface ReadWorkspaceFileOutput {
|
|
|
4
4
|
size: number;
|
|
5
5
|
redacted?: boolean;
|
|
6
6
|
redaction_categories?: string[];
|
|
7
|
+
relative_path?: string;
|
|
8
|
+
sha256?: string;
|
|
7
9
|
}
|
|
8
|
-
export
|
|
10
|
+
export interface ReadWorkspaceFileInput {
|
|
11
|
+
path: string;
|
|
12
|
+
session_id?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function readWorkspaceFile(input: string | ReadWorkspaceFileInput): ReadWorkspaceFileOutput;
|
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { getConfig } from "../config.js";
|
|
3
4
|
import { guardReadPath } from "../security/pathGuard.js";
|
|
4
5
|
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
5
6
|
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
6
|
-
|
|
7
|
+
import { PatchWardenError } from "../errors.js";
|
|
8
|
+
import { readDirectSession } from "../direct/directSessionStore.js";
|
|
9
|
+
import { guardDirectSessionActive, guardDirectReadPath, isBinaryFile, guardDirectFileSize } from "../direct/directGuards.js";
|
|
10
|
+
import { resolveToolProfile } from "./toolCatalog.js";
|
|
11
|
+
export function readWorkspaceFile(input) {
|
|
7
12
|
const config = getConfig();
|
|
13
|
+
// Support both old string API and new object API
|
|
14
|
+
const relativePath = typeof input === "string" ? input : input.path;
|
|
15
|
+
const sessionId = typeof input === "string" ? undefined : input.session_id;
|
|
16
|
+
// Direct mode: session_id provided
|
|
17
|
+
if (sessionId) {
|
|
18
|
+
return readWorkspaceFileDirect(relativePath, sessionId);
|
|
19
|
+
}
|
|
20
|
+
// In chatgpt_direct profile, session_id is required
|
|
21
|
+
const profile = resolveToolProfile(config.toolProfile);
|
|
22
|
+
if (profile === "chatgpt_direct") {
|
|
23
|
+
throw new PatchWardenError("session_id_required", "session_id is required when using the chatgpt_direct profile.", "Provide a session_id from create_direct_session.", true, { operation: "read_workspace_file" });
|
|
24
|
+
}
|
|
25
|
+
// Compatibility mode: no session_id, original behavior
|
|
8
26
|
const safePath = guardReadPath(relativePath, config.workspaceRoot);
|
|
9
27
|
guardSensitivePath(safePath);
|
|
10
28
|
const stat = statSync(safePath);
|
|
@@ -24,3 +42,31 @@ export function readWorkspaceFile(relativePath) {
|
|
|
24
42
|
redaction_categories: redaction.redaction_categories,
|
|
25
43
|
};
|
|
26
44
|
}
|
|
45
|
+
function readWorkspaceFileDirect(relativePath, sessionId) {
|
|
46
|
+
const config = getConfig();
|
|
47
|
+
const session = readDirectSession(sessionId);
|
|
48
|
+
guardDirectSessionActive(session);
|
|
49
|
+
// Resolve path within session's repo
|
|
50
|
+
const safePath = guardDirectReadPath(relativePath, session.resolved_repo_path, config.workspaceRoot);
|
|
51
|
+
if (!existsSync(safePath)) {
|
|
52
|
+
throw new PatchWardenError("file_not_found", `File "${relativePath}" not found in session repo.`, "Use search_workspace to find available files.", true, { path: relativePath, session_id: sessionId });
|
|
53
|
+
}
|
|
54
|
+
const stat = statSync(safePath);
|
|
55
|
+
guardDirectFileSize(stat.size);
|
|
56
|
+
// Block binary files
|
|
57
|
+
if (isBinaryFile(safePath)) {
|
|
58
|
+
throw new PatchWardenError("binary_file_blocked", `File "${relativePath}" appears to be a binary file.`, "Binary files cannot be read in Direct mode.", true, { path: relativePath });
|
|
59
|
+
}
|
|
60
|
+
const content = readFileSync(safePath, "utf-8");
|
|
61
|
+
const redaction = redactSensitiveContent(content);
|
|
62
|
+
const sha256 = createHash("sha256").update(content, "utf-8").digest("hex");
|
|
63
|
+
return {
|
|
64
|
+
path: safePath,
|
|
65
|
+
relative_path: relativePath,
|
|
66
|
+
content: redaction.content,
|
|
67
|
+
size: stat.size,
|
|
68
|
+
sha256,
|
|
69
|
+
redacted: redaction.redacted,
|
|
70
|
+
redaction_categories: redaction.redaction_categories,
|
|
71
|
+
};
|
|
72
|
+
}
|