patchwarden 1.1.0 → 1.5.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/README.en.md +83 -8
- package/README.md +78 -10
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +261 -255
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +45 -0
- package/dist/tools/evidencePack.js +375 -0
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/policy/projectPolicy.ts +344 -0
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +254 -252
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +251 -0
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { getConfig } from "../config.js";
|
|
5
|
+
import { guardReadPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
6
|
+
import { countRedactionsByCategory, redactSensitiveValue } from "../security/contentRedaction.js";
|
|
7
|
+
import { getProjectPolicySummary } from "../policy/projectPolicy.js";
|
|
8
|
+
import { getTaskLineage, type SafeTaskLineage } from "./taskLineage.js";
|
|
9
|
+
import { getLastToolCatalogSnapshot } from "./toolCatalog.js";
|
|
10
|
+
import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
11
|
+
import { PatchWardenError } from "../errors.js";
|
|
12
|
+
|
|
13
|
+
export interface SafeEvidencePack {
|
|
14
|
+
evidence_pack_id: string;
|
|
15
|
+
lineage_id: string;
|
|
16
|
+
generated_at: string;
|
|
17
|
+
path: string;
|
|
18
|
+
files: {
|
|
19
|
+
json: string;
|
|
20
|
+
markdown: string;
|
|
21
|
+
risk: string;
|
|
22
|
+
verify: string;
|
|
23
|
+
diffstat: string;
|
|
24
|
+
lineage: string;
|
|
25
|
+
attestation: string;
|
|
26
|
+
redactions: string;
|
|
27
|
+
};
|
|
28
|
+
lineage: SafeTaskLineage;
|
|
29
|
+
policy: {
|
|
30
|
+
valid: boolean;
|
|
31
|
+
issue_count: number;
|
|
32
|
+
release_readiness: unknown;
|
|
33
|
+
};
|
|
34
|
+
catalog: {
|
|
35
|
+
server_version: string;
|
|
36
|
+
schema_epoch: string;
|
|
37
|
+
tool_profile: string | null;
|
|
38
|
+
tool_count: number | null;
|
|
39
|
+
tool_manifest_sha256: string | null;
|
|
40
|
+
};
|
|
41
|
+
omitted: string[];
|
|
42
|
+
next_action: string;
|
|
43
|
+
bounded: true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function exportTaskEvidencePack(input: { lineage_id: string; max_items?: number }): SafeEvidencePack {
|
|
47
|
+
const lineageId = normalizeLineageId(input.lineage_id);
|
|
48
|
+
const maxItems = normalizeMaxItems(input.max_items);
|
|
49
|
+
const config = getConfig();
|
|
50
|
+
const lineage = getTaskLineage(lineageId, { max_items: maxItems });
|
|
51
|
+
const packDir = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs", lineageId);
|
|
52
|
+
guardWorkspacePath(packDir, config.workspaceRoot);
|
|
53
|
+
mkdirSync(packDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const policySummary = readPolicySummary(lineage.repo_path);
|
|
56
|
+
const catalog = safeCatalog();
|
|
57
|
+
const generatedAt = new Date().toISOString();
|
|
58
|
+
|
|
59
|
+
const filePaths = {
|
|
60
|
+
json: join(packDir, "evidence.json"),
|
|
61
|
+
markdown: join(packDir, "EVIDENCE.md"),
|
|
62
|
+
risk: join(packDir, "risk.json"),
|
|
63
|
+
verify: join(packDir, "verify.json"),
|
|
64
|
+
diffstat: join(packDir, "diffstat.json"),
|
|
65
|
+
lineage: join(packDir, "lineage.json"),
|
|
66
|
+
attestation: join(packDir, "attestation.json"),
|
|
67
|
+
redactions: join(packDir, "redactions.json"),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Build v2 structured artifact contents (raw, before redaction).
|
|
71
|
+
const riskContent = buildRiskContent(lineage);
|
|
72
|
+
const verifyContent = buildVerifyContent(lineage);
|
|
73
|
+
const diffstatContent = buildDiffstatContent(lineage, config);
|
|
74
|
+
const lineageSummary = buildLineageSummary(lineage);
|
|
75
|
+
const attestationContent = buildAttestation(catalog, generatedAt);
|
|
76
|
+
|
|
77
|
+
// Count redactions across all v2 raw contents for the audit trail.
|
|
78
|
+
const v2RawPayloads = [
|
|
79
|
+
riskContent,
|
|
80
|
+
verifyContent,
|
|
81
|
+
diffstatContent,
|
|
82
|
+
lineageSummary,
|
|
83
|
+
attestationContent,
|
|
84
|
+
].map((value) => JSON.stringify(value));
|
|
85
|
+
const aggregated = new Map<string, { category: string; reason: string; count: number }>();
|
|
86
|
+
for (const raw of v2RawPayloads) {
|
|
87
|
+
for (const entry of countRedactionsByCategory(raw)) {
|
|
88
|
+
const existing = aggregated.get(entry.category);
|
|
89
|
+
if (existing) existing.count += entry.count;
|
|
90
|
+
else aggregated.set(entry.category, { category: entry.category, reason: entry.reason, count: entry.count });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const redactionsList = [...aggregated.values()];
|
|
94
|
+
const redactionsContent = {
|
|
95
|
+
redactions: redactionsList,
|
|
96
|
+
total_redacted: redactionsList.reduce((sum, entry) => sum + entry.count, 0),
|
|
97
|
+
bounded: true,
|
|
98
|
+
note: "Only categories and counts are recorded; original secret values are never persisted.",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Redact and write each v2 file.
|
|
102
|
+
writeFileSync(filePaths.risk, JSON.stringify(redactSensitiveValue(riskContent).value, null, 2) + "\n", "utf-8");
|
|
103
|
+
writeFileSync(filePaths.verify, JSON.stringify(redactSensitiveValue(verifyContent).value, null, 2) + "\n", "utf-8");
|
|
104
|
+
writeFileSync(filePaths.diffstat, JSON.stringify(redactSensitiveValue(diffstatContent).value, null, 2) + "\n", "utf-8");
|
|
105
|
+
writeFileSync(filePaths.lineage, JSON.stringify(redactSensitiveValue(lineageSummary).value, null, 2) + "\n", "utf-8");
|
|
106
|
+
writeFileSync(filePaths.attestation, JSON.stringify(redactSensitiveValue(attestationContent).value, null, 2) + "\n", "utf-8");
|
|
107
|
+
writeFileSync(filePaths.redactions, JSON.stringify(redactSensitiveValue(redactionsContent).value, null, 2) + "\n", "utf-8");
|
|
108
|
+
|
|
109
|
+
const safePack = redactSensitiveValue({
|
|
110
|
+
evidence_pack_id: `evidence_${lineageId}`,
|
|
111
|
+
lineage_id: lineageId,
|
|
112
|
+
generated_at: generatedAt,
|
|
113
|
+
path: packDir,
|
|
114
|
+
files: filePaths,
|
|
115
|
+
lineage,
|
|
116
|
+
policy: {
|
|
117
|
+
valid: policySummary.valid,
|
|
118
|
+
issue_count: policySummary.issues.length,
|
|
119
|
+
release_readiness: policySummary.release_readiness,
|
|
120
|
+
},
|
|
121
|
+
catalog,
|
|
122
|
+
omitted: [
|
|
123
|
+
"stdout",
|
|
124
|
+
"stderr",
|
|
125
|
+
"diff",
|
|
126
|
+
"verification command logs",
|
|
127
|
+
"full artifact markdown",
|
|
128
|
+
"sensitive file contents",
|
|
129
|
+
"raw secret values (redactions.json stores only categories and counts)",
|
|
130
|
+
"full diff content (diffstat.json stores only file-level line counts)",
|
|
131
|
+
],
|
|
132
|
+
next_action: buildNextAction(lineage),
|
|
133
|
+
bounded: true,
|
|
134
|
+
}).value as SafeEvidencePack;
|
|
135
|
+
|
|
136
|
+
writeFileSync(safePack.files.json, JSON.stringify(safePack, null, 2) + "\n", "utf-8");
|
|
137
|
+
writeFileSync(safePack.files.markdown, buildEvidenceMarkdown(safePack), "utf-8");
|
|
138
|
+
return safePack;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function listEvidencePacks(options: { max_items?: number } = {}): { evidence_packs: SafeEvidencePack[]; total: number; truncated: boolean } {
|
|
142
|
+
const maxItems = normalizeMaxItems(options.max_items);
|
|
143
|
+
const config = getConfig();
|
|
144
|
+
const root = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs");
|
|
145
|
+
guardWorkspacePath(root, config.workspaceRoot);
|
|
146
|
+
if (!existsSync(root)) return { evidence_packs: [], total: 0, truncated: false };
|
|
147
|
+
const packs = readdirSync(root, { withFileTypes: true })
|
|
148
|
+
.filter((entry) => entry.isDirectory())
|
|
149
|
+
.map((entry) => readEvidencePack(entry.name))
|
|
150
|
+
.filter((entry): entry is SafeEvidencePack => entry !== null)
|
|
151
|
+
.sort((a, b) => b.generated_at.localeCompare(a.generated_at));
|
|
152
|
+
return {
|
|
153
|
+
evidence_packs: packs.slice(0, maxItems),
|
|
154
|
+
total: packs.length,
|
|
155
|
+
truncated: packs.length > maxItems,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function readEvidencePack(lineageId: string): SafeEvidencePack | null {
|
|
160
|
+
const safeLineageId = normalizeLineageId(lineageId);
|
|
161
|
+
const config = getConfig();
|
|
162
|
+
const filePath = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs", safeLineageId, "evidence.json");
|
|
163
|
+
guardReadPath(filePath, config.workspaceRoot, ".patchwarden/evidence-packs");
|
|
164
|
+
if (!existsSync(filePath)) return null;
|
|
165
|
+
const raw = readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
|
|
166
|
+
return redactSensitiveValue(JSON.parse(raw)).value as SafeEvidencePack;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeLineageId(value: string): string {
|
|
170
|
+
const lineageId = String(value || "").trim();
|
|
171
|
+
if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
|
|
172
|
+
throw new PatchWardenError(
|
|
173
|
+
"invalid_lineage_id",
|
|
174
|
+
"lineage_id may contain only letters, numbers, underscores, and hyphens.",
|
|
175
|
+
"Pass a lineage_id returned by run_task_loop.",
|
|
176
|
+
true,
|
|
177
|
+
{ lineage_id: lineageId }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return lineageId;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeMaxItems(value: number | undefined): number {
|
|
184
|
+
if (value === undefined) return 12;
|
|
185
|
+
if (!Number.isInteger(value) || value < 1 || value > 50) {
|
|
186
|
+
throw new Error("max_items must be an integer from 1 to 50.");
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readPolicySummary(repoPath: string): { valid: boolean; issues: unknown[]; release_readiness: unknown } {
|
|
192
|
+
try {
|
|
193
|
+
const policy = getProjectPolicySummary(repoPath || ".");
|
|
194
|
+
return {
|
|
195
|
+
valid: Boolean(policy.valid),
|
|
196
|
+
issues: Array.isArray(policy.issues) ? policy.issues : [],
|
|
197
|
+
release_readiness: policy.release_readiness || null,
|
|
198
|
+
};
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
valid: false,
|
|
202
|
+
issues: [{ code: "policy_unavailable", message: err instanceof Error ? err.message : String(err) }],
|
|
203
|
+
release_readiness: null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function safeCatalog(): SafeEvidencePack["catalog"] {
|
|
209
|
+
try {
|
|
210
|
+
const catalog = getLastToolCatalogSnapshot();
|
|
211
|
+
if (!catalog) throw new Error("catalog snapshot unavailable");
|
|
212
|
+
return {
|
|
213
|
+
server_version: catalog.server_version,
|
|
214
|
+
schema_epoch: catalog.schema_epoch,
|
|
215
|
+
tool_profile: catalog.tool_profile,
|
|
216
|
+
tool_count: catalog.tool_count,
|
|
217
|
+
tool_manifest_sha256: catalog.tool_manifest_sha256,
|
|
218
|
+
};
|
|
219
|
+
} catch {
|
|
220
|
+
return {
|
|
221
|
+
server_version: PATCHWARDEN_VERSION,
|
|
222
|
+
schema_epoch: TOOL_SCHEMA_EPOCH,
|
|
223
|
+
tool_profile: null,
|
|
224
|
+
tool_count: null,
|
|
225
|
+
tool_manifest_sha256: null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildNextAction(lineage: SafeTaskLineage): string {
|
|
231
|
+
if (lineage.stop_reason === "success") return "Use this evidence pack for review or release readiness.";
|
|
232
|
+
return lineage.next_action || "Review lineage before exporting release evidence.";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
type RiskSeverity = "high" | "medium" | "low";
|
|
236
|
+
|
|
237
|
+
interface RiskItem {
|
|
238
|
+
source: string;
|
|
239
|
+
task_id?: string;
|
|
240
|
+
severity: RiskSeverity;
|
|
241
|
+
category: string;
|
|
242
|
+
detail: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildRiskContent(lineage: SafeTaskLineage): { risks: RiskItem[]; count: number; by_severity: Record<RiskSeverity, number> } {
|
|
246
|
+
const risks: RiskItem[] = [];
|
|
247
|
+
for (const round of lineage.rounds) {
|
|
248
|
+
for (const check of round.fail_checks) {
|
|
249
|
+
risks.push({ source: "round", task_id: round.task_id, severity: "high", category: "fail_check", detail: check });
|
|
250
|
+
}
|
|
251
|
+
for (const check of round.warn_checks) {
|
|
252
|
+
risks.push({ source: "round", task_id: round.task_id, severity: "medium", category: "warn_check", detail: check });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const warning of lineage.warnings) {
|
|
256
|
+
risks.push({ source: "lineage", severity: "low", category: "warning", detail: warning });
|
|
257
|
+
}
|
|
258
|
+
const by_severity: Record<RiskSeverity, number> = {
|
|
259
|
+
high: risks.filter((risk) => risk.severity === "high").length,
|
|
260
|
+
medium: risks.filter((risk) => risk.severity === "medium").length,
|
|
261
|
+
low: risks.filter((risk) => risk.severity === "low").length,
|
|
262
|
+
};
|
|
263
|
+
return { risks, count: risks.length, by_severity };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface VerifyRecord {
|
|
267
|
+
source: string;
|
|
268
|
+
task_id?: string;
|
|
269
|
+
role?: string;
|
|
270
|
+
session_id?: string;
|
|
271
|
+
verification_status: string;
|
|
272
|
+
audit_verdict: string;
|
|
273
|
+
passed: boolean;
|
|
274
|
+
command_count?: number;
|
|
275
|
+
passed_commands?: number;
|
|
276
|
+
failed_commands?: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildVerifyContent(lineage: SafeTaskLineage): {
|
|
280
|
+
records: VerifyRecord[];
|
|
281
|
+
count: number;
|
|
282
|
+
summary: { total: number; passed: number; failed: number };
|
|
283
|
+
latest_status: string;
|
|
284
|
+
overall_passed: boolean;
|
|
285
|
+
} {
|
|
286
|
+
const records: VerifyRecord[] = [];
|
|
287
|
+
for (const round of lineage.rounds) {
|
|
288
|
+
records.push({
|
|
289
|
+
source: "round",
|
|
290
|
+
task_id: round.task_id,
|
|
291
|
+
role: round.role,
|
|
292
|
+
verification_status: round.verification_status,
|
|
293
|
+
audit_verdict: round.audit_verdict,
|
|
294
|
+
passed: round.verification_status === "passed",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
for (const session of lineage.tasks.direct_sessions) {
|
|
298
|
+
records.push({
|
|
299
|
+
source: "direct_session",
|
|
300
|
+
session_id: session.session_id,
|
|
301
|
+
verification_status: session.status || "unknown",
|
|
302
|
+
audit_verdict: session.audit_decision || "not_run",
|
|
303
|
+
passed: session.status === "passed",
|
|
304
|
+
command_count: session.command_count,
|
|
305
|
+
passed_commands: session.passed_commands,
|
|
306
|
+
failed_commands: session.failed_commands,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
const passed = records.filter((record) => record.passed).length;
|
|
310
|
+
return {
|
|
311
|
+
records,
|
|
312
|
+
count: records.length,
|
|
313
|
+
summary: { total: records.length, passed, failed: records.length - passed },
|
|
314
|
+
latest_status: lineage.verification.latest_status,
|
|
315
|
+
overall_passed: lineage.verification.passed,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
interface DiffstatFile {
|
|
320
|
+
path: string;
|
|
321
|
+
status: string;
|
|
322
|
+
additions: number;
|
|
323
|
+
deletions: number;
|
|
324
|
+
task_id: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildDiffstatContent(
|
|
328
|
+
lineage: SafeTaskLineage,
|
|
329
|
+
config: ReturnType<typeof getConfig>
|
|
330
|
+
): { files: DiffstatFile[]; count: number; totals: { additions: number; deletions: number } } {
|
|
331
|
+
const taskIds = [
|
|
332
|
+
lineage.tasks.main,
|
|
333
|
+
...lineage.tasks.fix,
|
|
334
|
+
...lineage.tasks.cleanup,
|
|
335
|
+
].filter((id): id is string => Boolean(id));
|
|
336
|
+
|
|
337
|
+
const files: DiffstatFile[] = [];
|
|
338
|
+
let totalAdditions = 0;
|
|
339
|
+
let totalDeletions = 0;
|
|
340
|
+
|
|
341
|
+
for (const taskId of taskIds) {
|
|
342
|
+
if (!/^[A-Za-z0-9_-]+$/.test(taskId)) continue;
|
|
343
|
+
const statsPath = resolve(config.workspaceRoot, config.tasksDir, taskId, "file-stats.json");
|
|
344
|
+
try {
|
|
345
|
+
guardWorkspacePath(statsPath, config.workspaceRoot);
|
|
346
|
+
if (!existsSync(statsPath)) continue;
|
|
347
|
+
const raw = readFileSync(statsPath, "utf-8").replace(/^\uFEFF/, "");
|
|
348
|
+
const stats = JSON.parse(raw) as { files?: Array<{ path?: unknown; status?: unknown; additions?: unknown; deletions?: unknown }> };
|
|
349
|
+
if (!Array.isArray(stats.files)) continue;
|
|
350
|
+
for (const file of stats.files) {
|
|
351
|
+
const additions = Number(file.additions) || 0;
|
|
352
|
+
const deletions = Number(file.deletions) || 0;
|
|
353
|
+
files.push({
|
|
354
|
+
path: String(file.path || ""),
|
|
355
|
+
status: String(file.status || "unknown"),
|
|
356
|
+
additions,
|
|
357
|
+
deletions,
|
|
358
|
+
task_id: taskId,
|
|
359
|
+
});
|
|
360
|
+
totalAdditions += additions;
|
|
361
|
+
totalDeletions += deletions;
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
// Skip unreadable or malformed file-stats.json entries.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
files,
|
|
370
|
+
count: files.length,
|
|
371
|
+
totals: { additions: totalAdditions, deletions: totalDeletions },
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildLineageSummary(lineage: SafeTaskLineage): {
|
|
376
|
+
lineage_id: string;
|
|
377
|
+
goal: string;
|
|
378
|
+
final_status: string;
|
|
379
|
+
stop_reason: string;
|
|
380
|
+
iterations_count: number;
|
|
381
|
+
task_counts: { main: number; fix: number; cleanup: number; direct_sessions: number };
|
|
382
|
+
verification: { latest_status: string; passed: boolean };
|
|
383
|
+
worktree: { isolation_mode: string; status: string };
|
|
384
|
+
agent_routing: { selected_agent: string } | null;
|
|
385
|
+
warnings_count: number;
|
|
386
|
+
errors_count: number;
|
|
387
|
+
truncated: boolean;
|
|
388
|
+
} {
|
|
389
|
+
return {
|
|
390
|
+
lineage_id: lineage.lineage_id,
|
|
391
|
+
goal: lineage.goal,
|
|
392
|
+
final_status: lineage.final_status,
|
|
393
|
+
stop_reason: lineage.stop_reason,
|
|
394
|
+
iterations_count: lineage.rounds.length,
|
|
395
|
+
task_counts: {
|
|
396
|
+
main: lineage.tasks.main ? 1 : 0,
|
|
397
|
+
fix: lineage.tasks.fix.length,
|
|
398
|
+
cleanup: lineage.tasks.cleanup.length,
|
|
399
|
+
direct_sessions: lineage.tasks.direct_sessions.length,
|
|
400
|
+
},
|
|
401
|
+
verification: lineage.verification,
|
|
402
|
+
worktree: {
|
|
403
|
+
isolation_mode: lineage.worktree.isolation_mode,
|
|
404
|
+
status: lineage.worktree.status,
|
|
405
|
+
},
|
|
406
|
+
agent_routing: lineage.agent_routing ? { selected_agent: lineage.agent_routing.selected_agent } : null,
|
|
407
|
+
warnings_count: lineage.warnings.length,
|
|
408
|
+
errors_count: lineage.errors.length,
|
|
409
|
+
truncated: lineage.truncated,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function readGitCommitShort(): string {
|
|
414
|
+
try {
|
|
415
|
+
return execSync("git rev-parse --short HEAD", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
416
|
+
} catch {
|
|
417
|
+
return "unknown";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function buildAttestation(
|
|
422
|
+
catalog: SafeEvidencePack["catalog"],
|
|
423
|
+
generatedAt: string
|
|
424
|
+
): {
|
|
425
|
+
patchwarden_version: string;
|
|
426
|
+
package_version: string;
|
|
427
|
+
commit: string;
|
|
428
|
+
node_version: string;
|
|
429
|
+
os: { platform: string; arch: string };
|
|
430
|
+
tool_profile: string | null;
|
|
431
|
+
schema_epoch: string;
|
|
432
|
+
generated_at: string;
|
|
433
|
+
} {
|
|
434
|
+
return {
|
|
435
|
+
patchwarden_version: PATCHWARDEN_VERSION,
|
|
436
|
+
package_version: PATCHWARDEN_VERSION,
|
|
437
|
+
commit: readGitCommitShort(),
|
|
438
|
+
node_version: process.version,
|
|
439
|
+
os: { platform: process.platform, arch: process.arch },
|
|
440
|
+
tool_profile: catalog.tool_profile,
|
|
441
|
+
schema_epoch: catalog.schema_epoch,
|
|
442
|
+
generated_at: generatedAt,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function buildEvidenceMarkdown(pack: SafeEvidencePack): string {
|
|
447
|
+
const direct = pack.lineage.tasks.direct_sessions
|
|
448
|
+
.map((entry) => `- ${entry.session_id}: status=${entry.status || "unknown"}, audit=${entry.audit_decision || "unknown"}`)
|
|
449
|
+
.join("\n");
|
|
450
|
+
const rounds = pack.lineage.rounds
|
|
451
|
+
.map((round) => `- ${round.role} ${round.task_id}: status=${round.status}, verification=${round.verification_status}, audit=${round.audit_verdict}`)
|
|
452
|
+
.join("\n");
|
|
453
|
+
return [
|
|
454
|
+
"# PatchWarden Evidence Pack",
|
|
455
|
+
"",
|
|
456
|
+
`- Evidence pack: ${pack.evidence_pack_id}`,
|
|
457
|
+
`- Lineage: ${pack.lineage_id}`,
|
|
458
|
+
`- Generated: ${pack.generated_at}`,
|
|
459
|
+
`- Final status: ${pack.lineage.final_status}`,
|
|
460
|
+
`- Stop reason: ${pack.lineage.stop_reason}`,
|
|
461
|
+
`- Worktree: ${pack.lineage.worktree.isolation_mode} (${pack.lineage.worktree.status})`,
|
|
462
|
+
`- Tool manifest: ${pack.catalog.tool_manifest_sha256 || "unavailable"}`,
|
|
463
|
+
"",
|
|
464
|
+
"## Verification Rounds",
|
|
465
|
+
rounds || "- None.",
|
|
466
|
+
"",
|
|
467
|
+
"## Direct Evidence",
|
|
468
|
+
direct || "- None.",
|
|
469
|
+
"",
|
|
470
|
+
"## Policy And Release",
|
|
471
|
+
`- Policy valid: ${pack.policy.valid}`,
|
|
472
|
+
`- Policy issue count: ${pack.policy.issue_count}`,
|
|
473
|
+
"",
|
|
474
|
+
"## Evidence Pack v2 Files",
|
|
475
|
+
"- `evidence.json` — full bounded evidence pack (machine-readable).",
|
|
476
|
+
"- `EVIDENCE.md` — human-readable evidence summary.",
|
|
477
|
+
"- `risk.json` — aggregated risk items with severity (high/medium/low).",
|
|
478
|
+
"- `verify.json` — structured verification records per round and direct session.",
|
|
479
|
+
"- `diffstat.json` — file-level additions/deletions without full diff content.",
|
|
480
|
+
"- `lineage.json` — bounded lineage summary (goal, status, task counts).",
|
|
481
|
+
"- `attestation.json` — version, commit, Node/OS, tool profile, schema epoch.",
|
|
482
|
+
"- `redactions.json` — redaction categories and counts (no original secret values).",
|
|
483
|
+
"",
|
|
484
|
+
"## Omitted",
|
|
485
|
+
...pack.omitted.map((item) => `- ${item}`),
|
|
486
|
+
"",
|
|
487
|
+
`Next action: ${pack.next_action}`,
|
|
488
|
+
"",
|
|
489
|
+
].join("\n");
|
|
490
|
+
}
|
|
@@ -37,9 +37,9 @@ export interface FinalizeDirectSessionOutput {
|
|
|
37
37
|
|
|
38
38
|
// ── Main function ──────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
export function finalizeDirectSession(
|
|
40
|
+
export async function finalizeDirectSession(
|
|
41
41
|
input: FinalizeDirectSessionInput
|
|
42
|
-
): FinalizeDirectSessionOutput {
|
|
42
|
+
): Promise<FinalizeDirectSessionOutput> {
|
|
43
43
|
const { session_id } = input;
|
|
44
44
|
|
|
45
45
|
// 1. Read session and guard active (not expired, not finalized)
|
|
@@ -47,10 +47,10 @@ export function finalizeDirectSession(
|
|
|
47
47
|
guardDirectSessionActive(session);
|
|
48
48
|
|
|
49
49
|
// 2. Capture after snapshot
|
|
50
|
-
const afterSnapshot = captureRepoSnapshot(session.resolved_repo_path);
|
|
50
|
+
const afterSnapshot = await captureRepoSnapshot(session.resolved_repo_path);
|
|
51
51
|
|
|
52
52
|
// 3. Build change artifacts from before/after snapshots
|
|
53
|
-
const changeArtifacts = buildChangeArtifacts(
|
|
53
|
+
const changeArtifacts = await buildChangeArtifacts(
|
|
54
54
|
session.resolved_repo_path,
|
|
55
55
|
session.workspace_snapshot_before,
|
|
56
56
|
afterSnapshot
|
|
@@ -64,7 +64,7 @@ export interface CreateSubgoalTaskOutput {
|
|
|
64
64
|
* @throws PatchWardenError("invalid_dependency") 当 depends_on 引用不存在的 subgoal
|
|
65
65
|
* @throws PatchWardenError("invalid_execution_mode") 当 execution_mode 为 "assess_only"
|
|
66
66
|
*/
|
|
67
|
-
export function createSubgoalTask(input: CreateSubgoalTaskInput): CreateSubgoalTaskOutput {
|
|
67
|
+
export async function createSubgoalTask(input: CreateSubgoalTaskInput): Promise<CreateSubgoalTaskOutput> {
|
|
68
68
|
// assess_only 不产生 task,无法关联到 subgoal
|
|
69
69
|
if (input.execution_mode === "assess_only") {
|
|
70
70
|
throw new PatchWardenError(
|
|
@@ -113,7 +113,7 @@ export function createSubgoalTask(input: CreateSubgoalTaskInput): CreateSubgoalT
|
|
|
113
113
|
// 3. 调用 createTask 创建关联任务(强制 execute 模式,确保返回 CreateTaskOutput)
|
|
114
114
|
let taskId: string;
|
|
115
115
|
try {
|
|
116
|
-
const taskResult = createTask({
|
|
116
|
+
const taskResult = await createTask({
|
|
117
117
|
plan_id: input.plan_id,
|
|
118
118
|
inline_plan: input.inline_plan,
|
|
119
119
|
plan_title: input.plan_title,
|
package/src/tools/healthCheck.ts
CHANGED
|
@@ -195,14 +195,14 @@ function directoryStatus(path: string, allowCreatable: boolean, createRoot?: str
|
|
|
195
195
|
try {
|
|
196
196
|
accessSync(parent, constants.R_OK | constants.W_OK);
|
|
197
197
|
return { available: true, path, readable: false, writable: true, reason: "Directory will be created on first use." };
|
|
198
|
-
} catch {}
|
|
198
|
+
} catch {} // probe failure handled by return value
|
|
199
199
|
}
|
|
200
200
|
return { available: false, path, readable: false, writable: false, reason: "Directory does not exist." };
|
|
201
201
|
}
|
|
202
202
|
let readable = false;
|
|
203
203
|
let writable = false;
|
|
204
|
-
try { accessSync(path, constants.R_OK); readable = true; } catch {}
|
|
205
|
-
try { accessSync(path, constants.W_OK); writable = true; } catch {}
|
|
204
|
+
try { accessSync(path, constants.R_OK); readable = true; } catch {} // probe failure handled by return value
|
|
205
|
+
try { accessSync(path, constants.W_OK); writable = true; } catch {} // probe failure handled by return value
|
|
206
206
|
return {
|
|
207
207
|
available: readable && writable,
|
|
208
208
|
path,
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import { readdirSync, statSync, existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { getConfig } from "../config.js";
|
|
4
|
-
import { guardPath } from "../security/pathGuard.js";
|
|
5
|
-
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
|
-
|
|
7
|
-
export interface ListEntry {
|
|
8
|
-
name: string;
|
|
9
|
-
type: "file" | "directory";
|
|
10
|
-
size?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ListWorkspaceOutput {
|
|
14
|
-
path: string;
|
|
15
|
-
entries: ListEntry[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const MAX_LIST_FILES = 200;
|
|
19
|
-
|
|
20
|
-
export function listWorkspace(relativePath?: string): ListWorkspaceOutput {
|
|
21
|
-
const config = getConfig();
|
|
22
|
-
const targetPath = relativePath
|
|
23
|
-
? resolve(config.workspaceRoot, relativePath)
|
|
24
|
-
: config.workspaceRoot;
|
|
25
|
-
|
|
26
|
-
const guarded = guardPath(targetPath, config.workspaceRoot);
|
|
27
|
-
|
|
28
|
-
if (!existsSync(guarded)) {
|
|
29
|
-
throw new Error(`Path not found: "${relativePath || "."}"`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const stat = statSync(guarded);
|
|
33
|
-
if (!stat.isDirectory()) {
|
|
34
|
-
throw new Error(`Not a directory: "${relativePath || "."}"`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const entries: ListEntry[] = [];
|
|
38
|
-
const raw = readdirSync(guarded);
|
|
39
|
-
|
|
40
|
-
for (const name of raw) {
|
|
41
|
-
// Skip hidden files and sensitive paths
|
|
42
|
-
const fullPath = resolve(guarded, name);
|
|
43
|
-
try {
|
|
44
|
-
guardSensitivePath(fullPath);
|
|
45
|
-
} catch {
|
|
46
|
-
continue; // skip sensitive files silently
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const s = statSync(fullPath);
|
|
51
|
-
entries.push({
|
|
52
|
-
name,
|
|
53
|
-
type: s.isDirectory() ? "directory" : "file",
|
|
54
|
-
size: s.isFile() ? s.size : undefined,
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
// can't stat, skip
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (entries.length >= MAX_LIST_FILES) break;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
path: relativePath || ".",
|
|
65
|
-
entries: entries.sort((a, b) => {
|
|
66
|
-
// directories first, then alphabetically
|
|
67
|
-
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
68
|
-
return a.name.localeCompare(b.name);
|
|
69
|
-
}),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
1
|
+
import { readdirSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
import { guardPath } from "../security/pathGuard.js";
|
|
5
|
+
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
|
+
|
|
7
|
+
export interface ListEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
type: "file" | "directory";
|
|
10
|
+
size?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ListWorkspaceOutput {
|
|
14
|
+
path: string;
|
|
15
|
+
entries: ListEntry[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_LIST_FILES = 200;
|
|
19
|
+
|
|
20
|
+
export function listWorkspace(relativePath?: string): ListWorkspaceOutput {
|
|
21
|
+
const config = getConfig();
|
|
22
|
+
const targetPath = relativePath
|
|
23
|
+
? resolve(config.workspaceRoot, relativePath)
|
|
24
|
+
: config.workspaceRoot;
|
|
25
|
+
|
|
26
|
+
const guarded = guardPath(targetPath, config.workspaceRoot);
|
|
27
|
+
|
|
28
|
+
if (!existsSync(guarded)) {
|
|
29
|
+
throw new Error(`Path not found: "${relativePath || "."}"`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stat = statSync(guarded);
|
|
33
|
+
if (!stat.isDirectory()) {
|
|
34
|
+
throw new Error(`Not a directory: "${relativePath || "."}"`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entries: ListEntry[] = [];
|
|
38
|
+
const raw = readdirSync(guarded);
|
|
39
|
+
|
|
40
|
+
for (const name of raw) {
|
|
41
|
+
// Skip hidden files and sensitive paths
|
|
42
|
+
const fullPath = resolve(guarded, name);
|
|
43
|
+
try {
|
|
44
|
+
guardSensitivePath(fullPath);
|
|
45
|
+
} catch {
|
|
46
|
+
continue; // skip sensitive files silently
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const s = statSync(fullPath);
|
|
51
|
+
entries.push({
|
|
52
|
+
name,
|
|
53
|
+
type: s.isDirectory() ? "directory" : "file",
|
|
54
|
+
size: s.isFile() ? s.size : undefined,
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
// can't stat, skip
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (entries.length >= MAX_LIST_FILES) break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
path: relativePath || ".",
|
|
65
|
+
entries: entries.sort((a, b) => {
|
|
66
|
+
// directories first, then alphabetically
|
|
67
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
68
|
+
return a.name.localeCompare(b.name);
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
}
|