patchwarden 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local-only confirmation for medium-risk assessment tickets.
|
|
4
|
+
*
|
|
5
|
+
* This command is deliberately not exposed through the MCP registry. It must
|
|
6
|
+
* be invoked by a person (or another explicitly trusted local workflow) in a
|
|
7
|
+
* terminal that uses the same PatchWarden configuration as the server.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local-only confirmation for medium-risk assessment tickets.
|
|
4
|
+
*
|
|
5
|
+
* This command is deliberately not exposed through the MCP registry. It must
|
|
6
|
+
* be invoked by a person (or another explicitly trusted local workflow) in a
|
|
7
|
+
* terminal that uses the same PatchWarden configuration as the server.
|
|
8
|
+
*/
|
|
9
|
+
import { loadConfig } from "../config.js";
|
|
10
|
+
import { confirmAssessment } from "./assessmentStore.js";
|
|
11
|
+
import { errorPayload } from "../errors.js";
|
|
12
|
+
loadConfig();
|
|
13
|
+
const assessmentId = process.argv[2];
|
|
14
|
+
if (!assessmentId || process.argv.length !== 3) {
|
|
15
|
+
console.error("Usage: patchwarden-confirm <full_assessment_id>");
|
|
16
|
+
console.error("The display-only assessment_short_id is not accepted.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const result = confirmAssessment(assessmentId);
|
|
21
|
+
console.log(JSON.stringify(result, null, 2));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error(JSON.stringify(errorPayload(error), null, 2));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -6,19 +6,40 @@ export interface PatchWardenConfig {
|
|
|
6
6
|
workspaceRoot: string;
|
|
7
7
|
plansDir: string;
|
|
8
8
|
tasksDir: string;
|
|
9
|
+
assessmentsDir: string;
|
|
10
|
+
assessmentTtlSeconds: number;
|
|
9
11
|
agents: Record<string, AgentConfig>;
|
|
10
12
|
allowedTestCommands: string[];
|
|
13
|
+
repoAllowedTestCommands?: Record<string, string[]>;
|
|
11
14
|
maxReadFileBytes: number;
|
|
12
15
|
defaultTaskTimeoutSeconds: number;
|
|
13
16
|
maxTaskTimeoutSeconds: number;
|
|
14
17
|
watcherStaleSeconds: number;
|
|
15
|
-
toolProfile?: "full" | "chatgpt_core";
|
|
18
|
+
toolProfile?: "full" | "chatgpt_core" | "chatgpt_direct";
|
|
16
19
|
repoAliases?: Record<string, string>;
|
|
20
|
+
enableAgentAssessment?: boolean;
|
|
21
|
+
agentAssessmentTimeoutSeconds?: number;
|
|
22
|
+
agentAssessmentMaxOutputBytes?: number;
|
|
23
|
+
agentAssessmentAgentName?: string;
|
|
24
|
+
enableDirectProfile?: boolean;
|
|
25
|
+
directSessionsDir: string;
|
|
26
|
+
directSessionTtlSeconds: number;
|
|
27
|
+
directMaxPatchBytes: number;
|
|
28
|
+
directMaxFileBytes: number;
|
|
29
|
+
directAllowedCommands?: string[];
|
|
30
|
+
repoDirectAllowedCommands?: Record<string, string[]>;
|
|
17
31
|
}
|
|
18
32
|
export declare function loadConfig(configPath?: string): PatchWardenConfig;
|
|
33
|
+
export declare function reloadConfig(configPath?: string): PatchWardenConfig;
|
|
19
34
|
export declare function getConfig(): PatchWardenConfig;
|
|
20
35
|
/** Resolve workspaceRoot: expand relative paths */
|
|
21
36
|
export declare function resolveWorkspaceRoot(config: PatchWardenConfig): string;
|
|
22
37
|
/** Resolve plans/tasks dirs relative to workspaceRoot */
|
|
23
38
|
export declare function getPlansDir(config: PatchWardenConfig): string;
|
|
24
39
|
export declare function getTasksDir(config: PatchWardenConfig): string;
|
|
40
|
+
export declare function getAssessmentsDir(config: PatchWardenConfig): string;
|
|
41
|
+
export declare function getRepoAllowedTestCommands(config: PatchWardenConfig, repoPath: string): string[];
|
|
42
|
+
export declare function getAllConfiguredTestCommands(config: PatchWardenConfig): string[];
|
|
43
|
+
export declare function getDirectSessionsDir(config: PatchWardenConfig): string;
|
|
44
|
+
export declare function getRepoDirectAllowedCommands(config: PatchWardenConfig, repoPath: string): string[];
|
|
45
|
+
export declare function getAllConfiguredDirectCommands(config: PatchWardenConfig): string[];
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
// ── Defaults ──────────────────────────────────────────────────────
|
|
4
4
|
const DEFAULT_CONFIG = {
|
|
5
5
|
workspaceRoot: process.cwd(),
|
|
6
6
|
plansDir: ".patchwarden/plans",
|
|
7
7
|
tasksDir: ".patchwarden/tasks",
|
|
8
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
9
|
+
assessmentTtlSeconds: 3600,
|
|
8
10
|
agents: {
|
|
9
11
|
codex: {
|
|
10
12
|
command: "codex",
|
|
@@ -33,17 +35,41 @@ const DEFAULT_CONFIG = {
|
|
|
33
35
|
"pytest",
|
|
34
36
|
"cargo test",
|
|
35
37
|
],
|
|
38
|
+
repoAllowedTestCommands: {},
|
|
36
39
|
maxReadFileBytes: 200_000,
|
|
37
40
|
defaultTaskTimeoutSeconds: 900,
|
|
38
41
|
maxTaskTimeoutSeconds: 3600,
|
|
39
42
|
watcherStaleSeconds: 30,
|
|
40
43
|
toolProfile: "full",
|
|
44
|
+
enableAgentAssessment: false,
|
|
45
|
+
agentAssessmentTimeoutSeconds: 120,
|
|
46
|
+
agentAssessmentMaxOutputBytes: 524288,
|
|
47
|
+
enableDirectProfile: false,
|
|
48
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
49
|
+
directSessionTtlSeconds: 3600,
|
|
50
|
+
directMaxPatchBytes: 200_000,
|
|
51
|
+
directMaxFileBytes: 500_000,
|
|
52
|
+
directAllowedCommands: [
|
|
53
|
+
"npm test",
|
|
54
|
+
"npm run test",
|
|
55
|
+
"npm run build",
|
|
56
|
+
"npm run lint",
|
|
57
|
+
"node --check main.js",
|
|
58
|
+
],
|
|
59
|
+
repoDirectAllowedCommands: {},
|
|
41
60
|
};
|
|
42
61
|
// ── Load config ───────────────────────────────────────────────────
|
|
43
62
|
let _config = null;
|
|
44
63
|
export function loadConfig(configPath) {
|
|
45
64
|
if (_config)
|
|
46
65
|
return _config;
|
|
66
|
+
return loadConfigInternal(configPath);
|
|
67
|
+
}
|
|
68
|
+
export function reloadConfig(configPath) {
|
|
69
|
+
_config = null;
|
|
70
|
+
return loadConfigInternal(configPath);
|
|
71
|
+
}
|
|
72
|
+
function loadConfigInternal(configPath) {
|
|
47
73
|
const explicitPath = configPath || process.env.PATCHWARDEN_CONFIG;
|
|
48
74
|
const candidatePaths = explicitPath
|
|
49
75
|
? [explicitPath]
|
|
@@ -87,6 +113,9 @@ export function getPlansDir(config) {
|
|
|
87
113
|
export function getTasksDir(config) {
|
|
88
114
|
return resolve(config.workspaceRoot, config.tasksDir);
|
|
89
115
|
}
|
|
116
|
+
export function getAssessmentsDir(config) {
|
|
117
|
+
return resolve(config.workspaceRoot, config.assessmentsDir);
|
|
118
|
+
}
|
|
90
119
|
function stripBom(value) {
|
|
91
120
|
return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value;
|
|
92
121
|
}
|
|
@@ -100,12 +129,37 @@ function normalizeConfig(config) {
|
|
|
100
129
|
if (!config.tasksDir || typeof config.tasksDir !== "string") {
|
|
101
130
|
throw new Error("tasksDir must be a non-empty string");
|
|
102
131
|
}
|
|
132
|
+
if (!config.assessmentsDir || typeof config.assessmentsDir !== "string") {
|
|
133
|
+
throw new Error("assessmentsDir must be a non-empty string");
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isInteger(config.assessmentTtlSeconds) || config.assessmentTtlSeconds < 60 || config.assessmentTtlSeconds > 86400) {
|
|
136
|
+
throw new Error("assessmentTtlSeconds must be an integer from 60 to 86400");
|
|
137
|
+
}
|
|
103
138
|
if (!config.agents || typeof config.agents !== "object") {
|
|
104
139
|
throw new Error("agents must be an object");
|
|
105
140
|
}
|
|
106
141
|
if (!Array.isArray(config.allowedTestCommands)) {
|
|
107
142
|
throw new Error("allowedTestCommands must be an array");
|
|
108
143
|
}
|
|
144
|
+
if (config.allowedTestCommands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
145
|
+
throw new Error("allowedTestCommands must contain only non-empty command strings");
|
|
146
|
+
}
|
|
147
|
+
if (!config.repoAllowedTestCommands || typeof config.repoAllowedTestCommands !== "object" || Array.isArray(config.repoAllowedTestCommands)) {
|
|
148
|
+
throw new Error("repoAllowedTestCommands must be an object mapping workspace-relative repository paths to command arrays");
|
|
149
|
+
}
|
|
150
|
+
const repoAllowedTestCommands = {};
|
|
151
|
+
for (const [repoKey, commands] of Object.entries(config.repoAllowedTestCommands)) {
|
|
152
|
+
const normalizedKey = normalizeRepoKey(repoKey);
|
|
153
|
+
const resolvedRepo = resolve(config.workspaceRoot, normalizedKey);
|
|
154
|
+
const relativeRepo = relative(resolve(config.workspaceRoot), resolvedRepo);
|
|
155
|
+
if (isAbsolute(repoKey) || relativeRepo === ".." || relativeRepo.startsWith(`..${sep}`) || isAbsolute(relativeRepo)) {
|
|
156
|
+
throw new Error(`repoAllowedTestCommands key must stay inside workspaceRoot: "${repoKey}"`);
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
159
|
+
throw new Error(`repoAllowedTestCommands["${repoKey}"] must be an array of non-empty command strings`);
|
|
160
|
+
}
|
|
161
|
+
repoAllowedTestCommands[normalizedKey] = [...new Set(commands.map((command) => command.trim()))];
|
|
162
|
+
}
|
|
109
163
|
if (!Number.isFinite(config.maxReadFileBytes) || config.maxReadFileBytes <= 0) {
|
|
110
164
|
throw new Error("maxReadFileBytes must be a positive number");
|
|
111
165
|
}
|
|
@@ -121,11 +175,110 @@ function normalizeConfig(config) {
|
|
|
121
175
|
if (!Number.isInteger(config.watcherStaleSeconds) || config.watcherStaleSeconds < 5 || config.watcherStaleSeconds > 3600) {
|
|
122
176
|
throw new Error("watcherStaleSeconds must be an integer from 5 to 3600");
|
|
123
177
|
}
|
|
124
|
-
if (config.toolProfile !== undefined && config.toolProfile !== "full" && config.toolProfile !== "chatgpt_core") {
|
|
125
|
-
throw new Error('toolProfile must be "full" or "
|
|
178
|
+
if (config.toolProfile !== undefined && config.toolProfile !== "full" && config.toolProfile !== "chatgpt_core" && config.toolProfile !== "chatgpt_direct") {
|
|
179
|
+
throw new Error('toolProfile must be "full", "chatgpt_core", or "chatgpt_direct"');
|
|
180
|
+
}
|
|
181
|
+
if (config.enableAgentAssessment !== undefined && typeof config.enableAgentAssessment !== "boolean") {
|
|
182
|
+
throw new Error("enableAgentAssessment must be a boolean");
|
|
183
|
+
}
|
|
184
|
+
if (config.agentAssessmentTimeoutSeconds !== undefined) {
|
|
185
|
+
if (!Number.isInteger(config.agentAssessmentTimeoutSeconds) || config.agentAssessmentTimeoutSeconds < 10 || config.agentAssessmentTimeoutSeconds > 600) {
|
|
186
|
+
throw new Error("agentAssessmentTimeoutSeconds must be an integer from 10 to 600");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (config.agentAssessmentMaxOutputBytes !== undefined) {
|
|
190
|
+
if (!Number.isInteger(config.agentAssessmentMaxOutputBytes) || config.agentAssessmentMaxOutputBytes < 16384 || config.agentAssessmentMaxOutputBytes > 8388608) {
|
|
191
|
+
throw new Error("agentAssessmentMaxOutputBytes must be an integer from 16384 to 8388608");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (config.agentAssessmentAgentName !== undefined && typeof config.agentAssessmentAgentName !== "string") {
|
|
195
|
+
throw new Error("agentAssessmentAgentName must be a string");
|
|
196
|
+
}
|
|
197
|
+
if (config.enableDirectProfile !== undefined && typeof config.enableDirectProfile !== "boolean") {
|
|
198
|
+
throw new Error("enableDirectProfile must be a boolean");
|
|
199
|
+
}
|
|
200
|
+
if (!config.directSessionsDir || typeof config.directSessionsDir !== "string") {
|
|
201
|
+
throw new Error("directSessionsDir must be a non-empty string");
|
|
202
|
+
}
|
|
203
|
+
if (!Number.isInteger(config.directSessionTtlSeconds) || config.directSessionTtlSeconds < 60 || config.directSessionTtlSeconds > 86400) {
|
|
204
|
+
throw new Error("directSessionTtlSeconds must be an integer from 60 to 86400");
|
|
205
|
+
}
|
|
206
|
+
if (!Number.isInteger(config.directMaxPatchBytes) || config.directMaxPatchBytes <= 0) {
|
|
207
|
+
throw new Error("directMaxPatchBytes must be a positive integer");
|
|
208
|
+
}
|
|
209
|
+
if (!Number.isInteger(config.directMaxFileBytes) || config.directMaxFileBytes <= 0) {
|
|
210
|
+
throw new Error("directMaxFileBytes must be a positive integer");
|
|
211
|
+
}
|
|
212
|
+
if (config.directAllowedCommands !== undefined) {
|
|
213
|
+
if (!Array.isArray(config.directAllowedCommands)) {
|
|
214
|
+
throw new Error("directAllowedCommands must be an array");
|
|
215
|
+
}
|
|
216
|
+
if (config.directAllowedCommands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
217
|
+
throw new Error("directAllowedCommands must contain only non-empty command strings");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (config.repoDirectAllowedCommands !== undefined) {
|
|
221
|
+
if (typeof config.repoDirectAllowedCommands !== "object" || Array.isArray(config.repoDirectAllowedCommands)) {
|
|
222
|
+
throw new Error("repoDirectAllowedCommands must be an object mapping workspace-relative repository paths to command arrays");
|
|
223
|
+
}
|
|
224
|
+
const repoDirectAllowedCommands = {};
|
|
225
|
+
for (const [repoKey, commands] of Object.entries(config.repoDirectAllowedCommands)) {
|
|
226
|
+
const normalizedKey = normalizeRepoKey(repoKey);
|
|
227
|
+
const resolvedRepo = resolve(config.workspaceRoot, normalizedKey);
|
|
228
|
+
const relativeRepo = relative(resolve(config.workspaceRoot), resolvedRepo);
|
|
229
|
+
if (isAbsolute(repoKey) || relativeRepo === ".." || relativeRepo.startsWith(`..${sep}`) || isAbsolute(relativeRepo)) {
|
|
230
|
+
throw new Error(`repoDirectAllowedCommands key must stay inside workspaceRoot: "${repoKey}"`);
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
233
|
+
throw new Error(`repoDirectAllowedCommands["${repoKey}"] must be an array of non-empty command strings`);
|
|
234
|
+
}
|
|
235
|
+
repoDirectAllowedCommands[normalizedKey] = [...new Set(commands.map((command) => command.trim()))];
|
|
236
|
+
}
|
|
237
|
+
config.repoDirectAllowedCommands = repoDirectAllowedCommands;
|
|
126
238
|
}
|
|
127
239
|
return {
|
|
128
240
|
...config,
|
|
129
241
|
workspaceRoot: resolve(config.workspaceRoot),
|
|
242
|
+
allowedTestCommands: [...new Set(config.allowedTestCommands.map((command) => command.trim()))],
|
|
243
|
+
repoAllowedTestCommands,
|
|
130
244
|
};
|
|
131
245
|
}
|
|
246
|
+
export function getRepoAllowedTestCommands(config, repoPath) {
|
|
247
|
+
const target = comparablePath(resolve(repoPath));
|
|
248
|
+
for (const [repoKey, commands] of Object.entries(config.repoAllowedTestCommands || {})) {
|
|
249
|
+
if (comparablePath(resolve(config.workspaceRoot, repoKey)) === target)
|
|
250
|
+
return [...commands];
|
|
251
|
+
}
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
export function getAllConfiguredTestCommands(config) {
|
|
255
|
+
return [...new Set([
|
|
256
|
+
...config.allowedTestCommands,
|
|
257
|
+
...Object.values(config.repoAllowedTestCommands || {}).flat(),
|
|
258
|
+
])];
|
|
259
|
+
}
|
|
260
|
+
export function getDirectSessionsDir(config) {
|
|
261
|
+
return resolve(config.workspaceRoot, config.directSessionsDir);
|
|
262
|
+
}
|
|
263
|
+
export function getRepoDirectAllowedCommands(config, repoPath) {
|
|
264
|
+
const target = comparablePath(resolve(repoPath));
|
|
265
|
+
for (const [repoKey, commands] of Object.entries(config.repoDirectAllowedCommands || {})) {
|
|
266
|
+
if (comparablePath(resolve(config.workspaceRoot, repoKey)) === target)
|
|
267
|
+
return [...commands];
|
|
268
|
+
}
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
export function getAllConfiguredDirectCommands(config) {
|
|
272
|
+
return [...new Set([
|
|
273
|
+
...(config.directAllowedCommands || []),
|
|
274
|
+
...Object.values(config.repoDirectAllowedCommands || {}).flat(),
|
|
275
|
+
])];
|
|
276
|
+
}
|
|
277
|
+
function normalizeRepoKey(value) {
|
|
278
|
+
const trimmed = String(value).trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
279
|
+
return trimmed === "" ? "." : trimmed;
|
|
280
|
+
}
|
|
281
|
+
function comparablePath(value) {
|
|
282
|
+
const normalized = resolve(value).replace(/\\/g, "/");
|
|
283
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
284
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type DirectSessionVerificationRun } from "./directSessionStore.js";
|
|
2
|
+
export interface AuditCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
result: "pass" | "warn" | "fail";
|
|
5
|
+
detail: string;
|
|
6
|
+
reason_code?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DirectSessionAuditOutput {
|
|
9
|
+
session_id: string;
|
|
10
|
+
decision: "pass" | "warn" | "fail";
|
|
11
|
+
reason_codes: string[];
|
|
12
|
+
blocking_findings: string[];
|
|
13
|
+
warnings: string[];
|
|
14
|
+
evidence: {
|
|
15
|
+
changed_files_total: number;
|
|
16
|
+
verification_runs: DirectSessionVerificationRun[];
|
|
17
|
+
diff_path: string;
|
|
18
|
+
summary_path: string;
|
|
19
|
+
audit_path: string;
|
|
20
|
+
};
|
|
21
|
+
next_action: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function auditDirectSession(sessionId: string): DirectSessionAuditOutput;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
5
|
+
import { readDirectSession, getDirectSessionDir, } from "./directSessionStore.js";
|
|
6
|
+
// ── Main audit function ────────────────────────────────────────────
|
|
7
|
+
export function auditDirectSession(sessionId) {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
const session = readDirectSession(sessionId);
|
|
10
|
+
const sessionDir = getDirectSessionDir(sessionId);
|
|
11
|
+
const checks = [];
|
|
12
|
+
const reasonCodes = [];
|
|
13
|
+
const blockingFindings = [];
|
|
14
|
+
const warnings = [];
|
|
15
|
+
// Check 1: session finalized
|
|
16
|
+
checks.push({
|
|
17
|
+
name: "session_finalized",
|
|
18
|
+
result: session.finalized ? "pass" : "fail",
|
|
19
|
+
detail: session.finalized
|
|
20
|
+
? "Session has been finalized."
|
|
21
|
+
: "Session has not been finalized. Call finalize_direct_session first.",
|
|
22
|
+
reason_code: session.finalized ? undefined : "session_not_finalized",
|
|
23
|
+
});
|
|
24
|
+
// Get change artifacts if available
|
|
25
|
+
const artifacts = session.change_artifacts;
|
|
26
|
+
// Check 2: diff empty
|
|
27
|
+
const changedFilesTotal = artifacts?.changed_files?.length ?? 0;
|
|
28
|
+
checks.push({
|
|
29
|
+
name: "diff_empty",
|
|
30
|
+
result: changedFilesTotal === 0 ? "warn" : "pass",
|
|
31
|
+
detail: changedFilesTotal === 0
|
|
32
|
+
? "No file changes detected in this session."
|
|
33
|
+
: `${changedFilesTotal} file(s) changed.`,
|
|
34
|
+
reason_code: changedFilesTotal === 0 ? "empty_diff" : undefined,
|
|
35
|
+
});
|
|
36
|
+
if (artifacts) {
|
|
37
|
+
// Check 3: out-of-scope changes
|
|
38
|
+
const outOfScope = findOutOfScopeChanges(artifacts, session.resolved_repo_path);
|
|
39
|
+
checks.push({
|
|
40
|
+
name: "out_of_scope_changes",
|
|
41
|
+
result: outOfScope.length === 0 ? "pass" : "fail",
|
|
42
|
+
detail: outOfScope.length === 0
|
|
43
|
+
? "All changes are within the session repo_path."
|
|
44
|
+
: `${outOfScope.length} file(s) modified outside session repo_path: ${outOfScope.join(", ")}`,
|
|
45
|
+
reason_code: outOfScope.length > 0 ? "out_of_scope_changes" : undefined,
|
|
46
|
+
});
|
|
47
|
+
// Check 4: sensitive files
|
|
48
|
+
const sensitiveFiles = findSensitiveChanges(artifacts);
|
|
49
|
+
checks.push({
|
|
50
|
+
name: "sensitive_file_access",
|
|
51
|
+
result: sensitiveFiles.length === 0 ? "pass" : "fail",
|
|
52
|
+
detail: sensitiveFiles.length === 0
|
|
53
|
+
? "No sensitive files modified."
|
|
54
|
+
: `Sensitive files modified: ${sensitiveFiles.join(", ")}`,
|
|
55
|
+
reason_code: sensitiveFiles.length > 0 ? "sensitive_file_modified" : undefined,
|
|
56
|
+
});
|
|
57
|
+
// Check 5: node_modules modification
|
|
58
|
+
const nodeModulesChanges = findPathChanges(artifacts, "node_modules");
|
|
59
|
+
checks.push({
|
|
60
|
+
name: "node_modules_modified",
|
|
61
|
+
result: nodeModulesChanges.length === 0 ? "pass" : "fail",
|
|
62
|
+
detail: nodeModulesChanges.length === 0
|
|
63
|
+
? "No node_modules modifications."
|
|
64
|
+
: `node_modules modified: ${nodeModulesChanges.join(", ")}`,
|
|
65
|
+
reason_code: nodeModulesChanges.length > 0 ? "node_modules_modified" : undefined,
|
|
66
|
+
});
|
|
67
|
+
// Check 6: release/dist modification
|
|
68
|
+
const releaseChanges = findPathChanges(artifacts, "release");
|
|
69
|
+
const distChanges = findPathChanges(artifacts, "dist");
|
|
70
|
+
const artifactDirChanges = [...releaseChanges, ...distChanges];
|
|
71
|
+
const hasBuildArtifacts = artifacts.artifact_hygiene.counts.tracked_build_artifacts > 0;
|
|
72
|
+
checks.push({
|
|
73
|
+
name: "release_dist_modified",
|
|
74
|
+
result: artifactDirChanges.length === 0
|
|
75
|
+
? "pass"
|
|
76
|
+
: hasBuildArtifacts
|
|
77
|
+
? "warn"
|
|
78
|
+
: "fail",
|
|
79
|
+
detail: artifactDirChanges.length === 0
|
|
80
|
+
? "No release/dist modifications."
|
|
81
|
+
: `release/dist modified: ${artifactDirChanges.join(", ")}${hasBuildArtifacts ? " (build-generated)" : " (not build-generated)"}`,
|
|
82
|
+
reason_code: artifactDirChanges.length > 0
|
|
83
|
+
? hasBuildArtifacts
|
|
84
|
+
? "build_artifact_modified"
|
|
85
|
+
: "release_dist_manually_modified"
|
|
86
|
+
: undefined,
|
|
87
|
+
});
|
|
88
|
+
// Check 7: file deletion
|
|
89
|
+
const deletedFiles = artifacts.changed_files.filter((f) => f.change === "deleted");
|
|
90
|
+
checks.push({
|
|
91
|
+
name: "file_deletion",
|
|
92
|
+
result: deletedFiles.length === 0 ? "pass" : "fail",
|
|
93
|
+
detail: deletedFiles.length === 0
|
|
94
|
+
? "No files deleted."
|
|
95
|
+
: `Files deleted: ${deletedFiles.map((f) => f.path).join(", ")}`,
|
|
96
|
+
reason_code: deletedFiles.length > 0 ? "file_deleted" : undefined,
|
|
97
|
+
});
|
|
98
|
+
// Check 8: file rename
|
|
99
|
+
const renamedFiles = artifacts.changed_files.filter((f) => f.change === "renamed");
|
|
100
|
+
checks.push({
|
|
101
|
+
name: "file_rename",
|
|
102
|
+
result: renamedFiles.length === 0 ? "pass" : "fail",
|
|
103
|
+
detail: renamedFiles.length === 0
|
|
104
|
+
? "No files renamed."
|
|
105
|
+
: `Files renamed: ${renamedFiles.map((f) => `${f.old_path} → ${f.path}`).join(", ")}`,
|
|
106
|
+
reason_code: renamedFiles.length > 0 ? "file_renamed" : undefined,
|
|
107
|
+
});
|
|
108
|
+
// Check 9: package-lock / dependency changes
|
|
109
|
+
const packageLockChanges = artifacts.changed_files.filter((f) => f.path === "package-lock.json" ||
|
|
110
|
+
f.path === "package.json" ||
|
|
111
|
+
f.path === "yarn.lock" ||
|
|
112
|
+
f.path === "pnpm-lock.yaml");
|
|
113
|
+
checks.push({
|
|
114
|
+
name: "dependency_changes",
|
|
115
|
+
result: packageLockChanges.length === 0 ? "pass" : "warn",
|
|
116
|
+
detail: packageLockChanges.length === 0
|
|
117
|
+
? "No dependency file changes."
|
|
118
|
+
: `Dependency files changed: ${packageLockChanges.map((f) => f.path).join(", ")}`,
|
|
119
|
+
reason_code: packageLockChanges.length > 0 ? "dependency_file_changed" : undefined,
|
|
120
|
+
});
|
|
121
|
+
// Check 13: title vs changed files consistency (warning only)
|
|
122
|
+
if (session.title && changedFilesTotal > 0) {
|
|
123
|
+
checks.push({
|
|
124
|
+
name: "title_change_consistency",
|
|
125
|
+
result: "pass",
|
|
126
|
+
detail: `Session title: "${session.title}", ${changedFilesTotal} file(s) changed. (Semantic check not performed - deterministic audit only.)`,
|
|
127
|
+
reason_code: undefined,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
checks.push({
|
|
132
|
+
name: "title_change_consistency",
|
|
133
|
+
result: "pass",
|
|
134
|
+
detail: "No title provided or no changes to compare.",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Check 14: suspicious changes
|
|
138
|
+
const suspiciousCount = artifacts.artifact_hygiene.counts.suspicious_changes;
|
|
139
|
+
checks.push({
|
|
140
|
+
name: "suspicious_changes",
|
|
141
|
+
result: suspiciousCount === 0 ? "pass" : "warn",
|
|
142
|
+
detail: suspiciousCount === 0
|
|
143
|
+
? "No suspicious changes detected."
|
|
144
|
+
: `${suspiciousCount} suspicious change(s) detected.`,
|
|
145
|
+
reason_code: suspiciousCount > 0 ? "suspicious_changes" : undefined,
|
|
146
|
+
});
|
|
147
|
+
// Check 15: runtime generated files
|
|
148
|
+
const runtimeCount = artifacts.artifact_hygiene.counts.runtime_generated_files;
|
|
149
|
+
checks.push({
|
|
150
|
+
name: "runtime_generated_files",
|
|
151
|
+
result: runtimeCount === 0 ? "pass" : "warn",
|
|
152
|
+
detail: runtimeCount === 0
|
|
153
|
+
? "No runtime-generated files detected."
|
|
154
|
+
: `${runtimeCount} runtime-generated file(s) detected.`,
|
|
155
|
+
reason_code: runtimeCount > 0 ? "runtime_generated_files" : undefined,
|
|
156
|
+
});
|
|
157
|
+
// Check 16: tracked build artifacts
|
|
158
|
+
const buildArtifactCount = artifacts.artifact_hygiene.counts.tracked_build_artifacts;
|
|
159
|
+
checks.push({
|
|
160
|
+
name: "tracked_build_artifacts",
|
|
161
|
+
result: buildArtifactCount === 0 ? "pass" : "warn",
|
|
162
|
+
detail: buildArtifactCount === 0
|
|
163
|
+
? "No tracked build artifacts detected."
|
|
164
|
+
: `${buildArtifactCount} tracked build artifact(s) detected.`,
|
|
165
|
+
reason_code: buildArtifactCount > 0 ? "tracked_build_artifacts" : undefined,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Check 10: at least one verification command run
|
|
169
|
+
const verificationRuns = session.verification_runs || [];
|
|
170
|
+
const hasVerification = verificationRuns.length > 0;
|
|
171
|
+
const hasSourceChanges = artifacts
|
|
172
|
+
? artifacts.changed_files.some((f) => f.kind === "source")
|
|
173
|
+
: false;
|
|
174
|
+
checks.push({
|
|
175
|
+
name: "verification_run",
|
|
176
|
+
result: hasVerification
|
|
177
|
+
? "pass"
|
|
178
|
+
: hasSourceChanges
|
|
179
|
+
? "fail"
|
|
180
|
+
: "warn",
|
|
181
|
+
detail: hasVerification
|
|
182
|
+
? `${verificationRuns.length} verification command(s) run.`
|
|
183
|
+
: hasSourceChanges
|
|
184
|
+
? "Source files were modified but no verification commands were run."
|
|
185
|
+
: "No verification commands were run (no source changes detected).",
|
|
186
|
+
reason_code: !hasVerification
|
|
187
|
+
? hasSourceChanges
|
|
188
|
+
? "source_changes_without_verification"
|
|
189
|
+
: "no_verification_run"
|
|
190
|
+
: undefined,
|
|
191
|
+
});
|
|
192
|
+
// Check 11: verification commands passed
|
|
193
|
+
if (hasVerification) {
|
|
194
|
+
const allPassed = verificationRuns.every((r) => r.passed);
|
|
195
|
+
checks.push({
|
|
196
|
+
name: "verification_passed",
|
|
197
|
+
result: allPassed ? "pass" : "fail",
|
|
198
|
+
detail: allPassed
|
|
199
|
+
? "All verification commands passed."
|
|
200
|
+
: `${verificationRuns.filter((r) => !r.passed).length} verification command(s) failed.`,
|
|
201
|
+
reason_code: !allPassed ? "verification_failed" : undefined,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// Collect results
|
|
205
|
+
for (const check of checks) {
|
|
206
|
+
if (check.reason_code)
|
|
207
|
+
reasonCodes.push(check.reason_code);
|
|
208
|
+
if (check.result === "fail")
|
|
209
|
+
blockingFindings.push(`${check.name}: ${check.detail}`);
|
|
210
|
+
if (check.result === "warn")
|
|
211
|
+
warnings.push(`${check.name}: ${check.detail}`);
|
|
212
|
+
}
|
|
213
|
+
// Determine decision
|
|
214
|
+
const hasFail = checks.some((c) => c.result === "fail");
|
|
215
|
+
const hasWarn = checks.some((c) => c.result === "warn");
|
|
216
|
+
const decision = hasFail ? "fail" : hasWarn ? "warn" : "pass";
|
|
217
|
+
const diffPath = join(sessionDir, "diff.patch");
|
|
218
|
+
const summaryPath = join(sessionDir, "summary.md");
|
|
219
|
+
const auditPath = join(sessionDir, "audit.md");
|
|
220
|
+
const output = {
|
|
221
|
+
session_id: sessionId,
|
|
222
|
+
decision,
|
|
223
|
+
reason_codes: reasonCodes,
|
|
224
|
+
blocking_findings: blockingFindings,
|
|
225
|
+
warnings,
|
|
226
|
+
evidence: {
|
|
227
|
+
changed_files_total: changedFilesTotal,
|
|
228
|
+
verification_runs: verificationRuns,
|
|
229
|
+
diff_path: diffPath,
|
|
230
|
+
summary_path: summaryPath,
|
|
231
|
+
audit_path: auditPath,
|
|
232
|
+
},
|
|
233
|
+
next_action: decision === "pass"
|
|
234
|
+
? "Audit passed. Changes are safe to accept."
|
|
235
|
+
: decision === "warn"
|
|
236
|
+
? "Audit completed with warnings. Review the warnings before accepting changes."
|
|
237
|
+
: "Audit failed. Review the blocking findings and create a new session to fix issues.",
|
|
238
|
+
};
|
|
239
|
+
// Write audit.json and audit.md
|
|
240
|
+
writeFileSync(join(sessionDir, "audit.json"), JSON.stringify(output, null, 2), "utf-8");
|
|
241
|
+
writeFileSync(auditPath, formatAuditMd(output, checks, session), "utf-8");
|
|
242
|
+
return output;
|
|
243
|
+
}
|
|
244
|
+
// ── Helper functions ───────────────────────────────────────────────
|
|
245
|
+
function findOutOfScopeChanges(artifacts, resolvedRepoPath) {
|
|
246
|
+
const normalizedRepo = resolve(resolvedRepoPath);
|
|
247
|
+
return artifacts.changed_files
|
|
248
|
+
.filter((f) => {
|
|
249
|
+
const fullPath = resolve(normalizedRepo, f.path);
|
|
250
|
+
const rel = relative(normalizedRepo, fullPath);
|
|
251
|
+
return isAbsolute(rel) || rel.startsWith("..");
|
|
252
|
+
})
|
|
253
|
+
.map((f) => f.path);
|
|
254
|
+
}
|
|
255
|
+
function findSensitiveChanges(artifacts) {
|
|
256
|
+
return artifacts.changed_files
|
|
257
|
+
.filter((f) => isSensitivePath(f.path))
|
|
258
|
+
.map((f) => f.path);
|
|
259
|
+
}
|
|
260
|
+
function findPathChanges(artifacts, dirName) {
|
|
261
|
+
return artifacts.changed_files
|
|
262
|
+
.filter((f) => {
|
|
263
|
+
const normalized = f.path.replace(/\\/g, "/");
|
|
264
|
+
return normalized.startsWith(`${dirName}/`) || normalized.includes(`/${dirName}/`);
|
|
265
|
+
})
|
|
266
|
+
.map((f) => f.path);
|
|
267
|
+
}
|
|
268
|
+
function formatAuditMd(output, checks, session) {
|
|
269
|
+
const lines = [
|
|
270
|
+
"# Direct Session Audit Report",
|
|
271
|
+
"",
|
|
272
|
+
`**Session ID:** ${output.session_id}`,
|
|
273
|
+
`**Decision:** ${output.decision.toUpperCase()}`,
|
|
274
|
+
`**Changed files:** ${output.evidence.changed_files_total}`,
|
|
275
|
+
"",
|
|
276
|
+
"## Checks",
|
|
277
|
+
"",
|
|
278
|
+
"| Check | Result | Detail |",
|
|
279
|
+
"|---|---|---|",
|
|
280
|
+
];
|
|
281
|
+
for (const check of checks) {
|
|
282
|
+
const emoji = check.result === "pass" ? "PASS" : check.result === "warn" ? "WARN" : "FAIL";
|
|
283
|
+
lines.push(`| ${check.name} | ${emoji} | ${check.detail.replace(/\|/g, "\\|")} |`);
|
|
284
|
+
}
|
|
285
|
+
if (output.blocking_findings.length > 0) {
|
|
286
|
+
lines.push("", "## Blocking Findings", "");
|
|
287
|
+
for (const finding of output.blocking_findings) {
|
|
288
|
+
lines.push(`- ${finding}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (output.warnings.length > 0) {
|
|
292
|
+
lines.push("", "## Warnings", "");
|
|
293
|
+
for (const warning of output.warnings) {
|
|
294
|
+
lines.push(`- ${warning}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
lines.push("", "## Evidence", "");
|
|
298
|
+
lines.push(`- Diff: \`${output.evidence.diff_path}\``);
|
|
299
|
+
lines.push(`- Summary: \`${output.evidence.summary_path}\``);
|
|
300
|
+
lines.push(`- Verification runs: ${output.evidence.verification_runs.length}`);
|
|
301
|
+
if (output.evidence.verification_runs.length > 0) {
|
|
302
|
+
lines.push("", "### Verification Results", "");
|
|
303
|
+
for (const run of output.evidence.verification_runs) {
|
|
304
|
+
lines.push(`- **${run.command}**: ${run.passed ? "PASSED" : "FAILED"} (exit code: ${run.exit_code})`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
lines.push("", `**Next action:** ${output.next_action}`);
|
|
308
|
+
return lines.join("\n");
|
|
309
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DirectSessionRecord } from "./directSessionStore.js";
|
|
2
|
+
export declare function guardDirectSessionActive(session: DirectSessionRecord): void;
|
|
3
|
+
export declare function guardDirectSessionFinalized(session: DirectSessionRecord): void;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a relative path against the session's repo_path and verify it stays
|
|
6
|
+
* inside both the repo and the workspace root.
|
|
7
|
+
*/
|
|
8
|
+
export declare function guardDirectPath(filePath: string, resolvedRepoPath: string, workspaceRoot: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Guard a read path: must be inside repo, not sensitive, not binary.
|
|
11
|
+
*/
|
|
12
|
+
export declare function guardDirectReadPath(filePath: string, resolvedRepoPath: string, workspaceRoot: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Guard a write path: must be inside repo, not sensitive, not binary,
|
|
15
|
+
* not in node_modules, release, or dist directories.
|
|
16
|
+
*/
|
|
17
|
+
export declare function guardDirectWritePath(filePath: string, resolvedRepoPath: string, workspaceRoot: string): string;
|
|
18
|
+
export declare function guardDirectPatchSize(patchBytes: number): void;
|
|
19
|
+
export declare function guardDirectFileSize(fileBytes: number): void;
|
|
20
|
+
export declare function isBinaryFile(filePath: string): boolean;
|