guardrail-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/autopilot.test.d.ts +7 -0
- package/dist/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/__tests__/autopilot.test.js +156 -0
- package/dist/__tests__/tier-config.test.d.ts +9 -0
- package/dist/__tests__/tier-config.test.d.ts.map +1 -0
- package/dist/__tests__/tier-config.test.js +230 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash-inline.test.js +62 -0
- package/dist/__tests__/utils/hash.test.d.ts +3 -0
- package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash.test.js +95 -0
- package/dist/__tests__/utils/simple.test.d.ts +1 -0
- package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/simple.test.js +10 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils-simple.test.js +6 -0
- package/dist/__tests__/utils/utils.test.d.ts +15 -0
- package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils.test.js +172 -0
- package/dist/autopilot/autopilot-runner.d.ts +33 -0
- package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
- package/dist/autopilot/autopilot-runner.js +479 -0
- package/dist/autopilot/index.d.ts +6 -0
- package/dist/autopilot/index.d.ts.map +1 -0
- package/dist/autopilot/index.js +25 -0
- package/dist/autopilot/types.d.ts +102 -0
- package/dist/autopilot/types.d.ts.map +1 -0
- package/dist/autopilot/types.js +18 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +22 -0
- package/dist/cache/redis-cache.d.ts +145 -0
- package/dist/cache/redis-cache.d.ts.map +1 -0
- package/dist/cache/redis-cache.js +459 -0
- package/dist/ci/github-actions.d.ts +77 -0
- package/dist/ci/github-actions.d.ts.map +1 -0
- package/dist/ci/github-actions.js +277 -0
- package/dist/ci/index.d.ts +12 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +27 -0
- package/dist/ci/pre-commit.d.ts +65 -0
- package/dist/ci/pre-commit.d.ts.map +1 -0
- package/dist/ci/pre-commit.js +286 -0
- package/dist/entitlements.d.ts +149 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +464 -0
- package/dist/env.d.ts +113 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +204 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
- package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
- package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
- package/dist/fix-packs/generate-fix-packs.js +505 -0
- package/dist/fix-packs/index.d.ts +8 -0
- package/dist/fix-packs/index.d.ts.map +1 -0
- package/dist/fix-packs/index.js +23 -0
- package/dist/fix-packs/types.d.ts +113 -0
- package/dist/fix-packs/types.d.ts.map +1 -0
- package/dist/fix-packs/types.js +71 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/metrics/prometheus.d.ts +99 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +306 -0
- package/dist/quota-ledger.d.ts +119 -0
- package/dist/quota-ledger.d.ts.map +1 -0
- package/dist/quota-ledger.js +462 -0
- package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
- package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
- package/dist/rbac/__tests__/permissions.test.js +350 -0
- package/dist/rbac/index.d.ts +9 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +32 -0
- package/dist/rbac/permissions.d.ts +71 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +247 -0
- package/dist/rbac/types.d.ts +69 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +213 -0
- package/dist/tier-config.d.ts +203 -0
- package/dist/tier-config.d.ts.map +1 -0
- package/dist/tier-config.js +675 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +127 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
- package/dist/verified-autofix/format-validator.d.ts +101 -0
- package/dist/verified-autofix/format-validator.d.ts.map +1 -0
- package/dist/verified-autofix/format-validator.js +446 -0
- package/dist/verified-autofix/index.d.ts +14 -0
- package/dist/verified-autofix/index.d.ts.map +1 -0
- package/dist/verified-autofix/index.js +39 -0
- package/dist/verified-autofix/pipeline.d.ts +68 -0
- package/dist/verified-autofix/pipeline.d.ts.map +1 -0
- package/dist/verified-autofix/pipeline.js +330 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
- package/dist/verified-autofix/repo-fingerprint.js +396 -0
- package/dist/verified-autofix/workspace.d.ts +83 -0
- package/dist/verified-autofix/workspace.d.ts.map +1 -0
- package/dist/verified-autofix/workspace.js +454 -0
- package/dist/verified-autofix.d.ts +182 -0
- package/dist/verified-autofix.d.ts.map +1 -0
- package/dist/verified-autofix.js +1021 -0
- package/dist/visualization/dependency-graph.d.ts +79 -0
- package/dist/visualization/dependency-graph.d.ts.map +1 -0
- package/dist/visualization/dependency-graph.js +399 -0
- package/dist/visualization/index.d.ts +5 -0
- package/dist/visualization/index.d.ts.map +1 -0
- package/dist/visualization/index.js +20 -0
- package/package.json +29 -0
- package/src/__tests__/autopilot.test.ts +196 -0
- package/src/__tests__/tier-config.test.ts +289 -0
- package/src/__tests__/utils/hash-inline.test.ts +76 -0
- package/src/__tests__/utils/hash.test.ts +119 -0
- package/src/__tests__/utils/simple.test.ts +10 -0
- package/src/__tests__/utils/utils-simple.test.ts +5 -0
- package/src/__tests__/utils/utils.test.ts +203 -0
- package/src/autopilot/autopilot-runner.ts +503 -0
- package/src/autopilot/index.ts +6 -0
- package/src/autopilot/types.ts +119 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/redis-cache.d.ts +155 -0
- package/src/cache/redis-cache.d.ts.map +1 -0
- package/src/cache/redis-cache.ts +517 -0
- package/src/ci/github-actions.ts +335 -0
- package/src/ci/index.ts +12 -0
- package/src/ci/pre-commit.ts +338 -0
- package/src/db/usage-schema.prisma +114 -0
- package/src/entitlements.ts +570 -0
- package/src/env.d.ts +68 -0
- package/src/env.d.ts.map +1 -0
- package/src/env.ts +247 -0
- package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
- package/src/fix-packs/generate-fix-packs.ts +577 -0
- package/src/fix-packs/index.ts +8 -0
- package/src/fix-packs/types.ts +206 -0
- package/src/index.d.ts +7 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +12 -0
- package/src/metrics/prometheus.d.ts +104 -0
- package/src/metrics/prometheus.d.ts.map +1 -0
- package/src/metrics/prometheus.ts +446 -0
- package/src/quota-ledger.ts +548 -0
- package/src/rbac/__tests__/permissions.test.ts +446 -0
- package/src/rbac/index.ts +46 -0
- package/src/rbac/permissions.ts +301 -0
- package/src/rbac/types.ts +298 -0
- package/src/tier-config.json +157 -0
- package/src/tier-config.ts +815 -0
- package/src/types.d.ts +365 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +441 -0
- package/src/utils.d.ts +36 -0
- package/src/utils.d.ts.map +1 -0
- package/src/utils.ts +140 -0
- package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
- package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
- package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
- package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
- package/src/verified-autofix/format-validator.ts +517 -0
- package/src/verified-autofix/index.ts +63 -0
- package/src/verified-autofix/pipeline.ts +403 -0
- package/src/verified-autofix/repo-fingerprint.ts +459 -0
- package/src/verified-autofix/workspace.ts +531 -0
- package/src/verified-autofix.ts +1187 -0
- package/src/visualization/dependency-graph.d.ts +85 -0
- package/src/visualization/dependency-graph.d.ts.map +1 -0
- package/src/visualization/dependency-graph.ts +495 -0
- package/src/visualization/index.ts +5 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Validator - Strict Output Protocol Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Validates AI agent output format:
|
|
5
|
+
* 1. JSON shape validation (guardrail-v1 format)
|
|
6
|
+
* 2. Unified diff validity checking
|
|
7
|
+
* 3. Markdown fence stripping (forgiving)
|
|
8
|
+
* 4. Path safety validation
|
|
9
|
+
* 5. Stub/placeholder detection
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// TYPES
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface GuardrailV1Output {
|
|
19
|
+
format: 'guardrail-v1';
|
|
20
|
+
diff: string;
|
|
21
|
+
commands: string[];
|
|
22
|
+
tests: string[];
|
|
23
|
+
notes: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ValidationResult {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
errors: string[];
|
|
29
|
+
warnings: string[];
|
|
30
|
+
sanitized?: GuardrailV1Output;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiffValidationResult {
|
|
34
|
+
valid: boolean;
|
|
35
|
+
errors: string[];
|
|
36
|
+
hunks: ParsedHunk[];
|
|
37
|
+
filesAffected: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ParsedHunk {
|
|
41
|
+
file: string;
|
|
42
|
+
oldStart: number;
|
|
43
|
+
oldLines: number;
|
|
44
|
+
newStart: number;
|
|
45
|
+
newLines: number;
|
|
46
|
+
content: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// CONSTANTS
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
const STUB_PATTERNS = [
|
|
54
|
+
/TODO\s*:/i,
|
|
55
|
+
/FIXME\s*:/i,
|
|
56
|
+
/XXX\s*:/i,
|
|
57
|
+
/HACK\s*:/i,
|
|
58
|
+
/\bplaceholder\b/i,
|
|
59
|
+
/\bstub\b/i,
|
|
60
|
+
/\bnot\s+implemented\b/i,
|
|
61
|
+
/throw\s+new\s+Error\s*\(\s*['"`]Not implemented/i,
|
|
62
|
+
/\/\/\s*TODO/i,
|
|
63
|
+
/\/\*\s*TODO/i,
|
|
64
|
+
/console\.log\s*\(\s*['"`]TODO/i,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const UNSAFE_PATH_PATTERNS = [
|
|
68
|
+
/\.\.\//, // Parent directory traversal
|
|
69
|
+
/^\/etc\//, // System config
|
|
70
|
+
/^\/usr\//, // System binaries
|
|
71
|
+
/^\/var\//, // System var
|
|
72
|
+
/^\/root\//, // Root home
|
|
73
|
+
/^\/home\/(?![\w-]+\/)/, // Other users' homes
|
|
74
|
+
/^C:\\Windows\\/i, // Windows system
|
|
75
|
+
/^C:\\Program Files/i, // Windows programs
|
|
76
|
+
/node_modules\//, // Dependencies
|
|
77
|
+
/\.git\//, // Git internals
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const UNSAFE_COMMANDS = [
|
|
81
|
+
/\brm\s+-rf?\s+\//, // Delete root
|
|
82
|
+
/\brm\s+-rf?\s+~\//, // Delete home
|
|
83
|
+
/\bsudo\b/, // Elevated privileges
|
|
84
|
+
/\bchmod\s+777\b/, // Insecure permissions
|
|
85
|
+
/\bcurl\b.*\|\s*sh\b/, // Pipe to shell
|
|
86
|
+
/\bwget\b.*\|\s*sh\b/, // Pipe to shell
|
|
87
|
+
/\beval\s*\(/, // Code injection
|
|
88
|
+
/\bexec\s*\(/, // Code execution
|
|
89
|
+
/\b--force\b/, // Force flags (risky)
|
|
90
|
+
/\bgit\s+push\s+--force\b/, // Force push
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// MARKDOWN FENCE STRIPPING
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Strip markdown code fences from raw agent output (forgiving mode)
|
|
99
|
+
*/
|
|
100
|
+
export function stripMarkdownFences(raw: string): string {
|
|
101
|
+
let content = raw.trim();
|
|
102
|
+
|
|
103
|
+
// Remove ```json ... ``` wrapper
|
|
104
|
+
const jsonFenceMatch = content.match(/^```(?:json)?\s*([\s\S]*?)```\s*$/);
|
|
105
|
+
if (jsonFenceMatch && jsonFenceMatch[1]) {
|
|
106
|
+
content = jsonFenceMatch[1].trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove leading/trailing ``` if partial
|
|
110
|
+
if (content.startsWith('```json')) {
|
|
111
|
+
content = content.slice(7).trim();
|
|
112
|
+
} else if (content.startsWith('```')) {
|
|
113
|
+
content = content.slice(3).trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (content.endsWith('```')) {
|
|
117
|
+
content = content.slice(0, -3).trim();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return content;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// JSON SHAPE VALIDATION
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate the guardrail-v1 JSON shape
|
|
129
|
+
*/
|
|
130
|
+
export function validateJsonShape(obj: unknown): ValidationResult {
|
|
131
|
+
const errors: string[] = [];
|
|
132
|
+
const warnings: string[] = [];
|
|
133
|
+
|
|
134
|
+
if (!obj || typeof obj !== 'object') {
|
|
135
|
+
return { valid: false, errors: ['Input must be a JSON object'], warnings: [] };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const data = obj as Record<string, unknown>;
|
|
139
|
+
|
|
140
|
+
// Check format field
|
|
141
|
+
if (data['format'] !== 'guardrail-v1') {
|
|
142
|
+
errors.push(`Missing or invalid "format" field. Expected "guardrail-v1", got "${data['format']}"`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check diff field
|
|
146
|
+
if (typeof data['diff'] !== 'string') {
|
|
147
|
+
errors.push('Missing or invalid "diff" field. Must be a string.');
|
|
148
|
+
} else if (data['diff'].length === 0) {
|
|
149
|
+
warnings.push('Empty diff field - no changes to apply');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check commands field
|
|
153
|
+
if (!Array.isArray(data['commands'])) {
|
|
154
|
+
errors.push('Missing or invalid "commands" field. Must be an array.');
|
|
155
|
+
} else {
|
|
156
|
+
for (let i = 0; i < data['commands'].length; i++) {
|
|
157
|
+
if (typeof data['commands'][i] !== 'string') {
|
|
158
|
+
errors.push(`commands[${i}] must be a string`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check tests field
|
|
164
|
+
if (!Array.isArray(data['tests'])) {
|
|
165
|
+
errors.push('Missing or invalid "tests" field. Must be an array.');
|
|
166
|
+
} else {
|
|
167
|
+
for (let i = 0; i < data['tests'].length; i++) {
|
|
168
|
+
if (typeof data['tests'][i] !== 'string') {
|
|
169
|
+
errors.push(`tests[${i}] must be a string`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check notes field
|
|
175
|
+
if (typeof data['notes'] !== 'string') {
|
|
176
|
+
errors.push('Missing or invalid "notes" field. Must be a string.');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (errors.length > 0) {
|
|
180
|
+
return { valid: false, errors, warnings };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sanitized: GuardrailV1Output = {
|
|
184
|
+
format: 'guardrail-v1',
|
|
185
|
+
diff: data['diff'] as string,
|
|
186
|
+
commands: data['commands'] as string[],
|
|
187
|
+
tests: data['tests'] as string[],
|
|
188
|
+
notes: data['notes'] as string,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return { valid: true, errors: [], warnings, sanitized };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// UNIFIED DIFF VALIDATION
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Parse and validate unified diff format
|
|
200
|
+
*/
|
|
201
|
+
export function validateUnifiedDiff(diff: string): DiffValidationResult {
|
|
202
|
+
const errors: string[] = [];
|
|
203
|
+
const hunks: ParsedHunk[] = [];
|
|
204
|
+
const filesAffected: string[] = [];
|
|
205
|
+
|
|
206
|
+
if (!diff || diff.trim().length === 0) {
|
|
207
|
+
return { valid: true, errors: [], hunks: [], filesAffected: [] };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const lines = diff.split('\n');
|
|
211
|
+
let currentFile: string | null = null;
|
|
212
|
+
let currentHunk: ParsedHunk | null = null;
|
|
213
|
+
let hunkContent: string[] = [];
|
|
214
|
+
let lineIndex = 0;
|
|
215
|
+
|
|
216
|
+
while (lineIndex < lines.length) {
|
|
217
|
+
const line = lines[lineIndex] || '';
|
|
218
|
+
|
|
219
|
+
// File header: --- a/path or --- path
|
|
220
|
+
if (line.startsWith('--- ')) {
|
|
221
|
+
const filePath = line.slice(4).replace(/^[ab]\//, '').split('\t')[0];
|
|
222
|
+
if (filePath) {
|
|
223
|
+
currentFile = filePath;
|
|
224
|
+
}
|
|
225
|
+
lineIndex++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// File header: +++ b/path or +++ path
|
|
230
|
+
if (line.startsWith('+++ ')) {
|
|
231
|
+
const filePath = line.slice(4).replace(/^[ab]\//, '').split('\t')[0];
|
|
232
|
+
if (filePath && filePath !== '/dev/null') {
|
|
233
|
+
currentFile = filePath;
|
|
234
|
+
if (!filesAffected.includes(filePath)) {
|
|
235
|
+
filesAffected.push(filePath);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
lineIndex++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Hunk header: @@ -start,count +start,count @@
|
|
243
|
+
const hunkMatch = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
|
|
244
|
+
if (hunkMatch) {
|
|
245
|
+
// Save previous hunk
|
|
246
|
+
if (currentHunk && currentFile) {
|
|
247
|
+
currentHunk.content = hunkContent.join('\n');
|
|
248
|
+
hunks.push(currentHunk);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
currentHunk = {
|
|
252
|
+
file: currentFile || 'unknown',
|
|
253
|
+
oldStart: parseInt(hunkMatch[1] || '1', 10),
|
|
254
|
+
oldLines: parseInt(hunkMatch[2] || '1', 10),
|
|
255
|
+
newStart: parseInt(hunkMatch[3] || '1', 10),
|
|
256
|
+
newLines: parseInt(hunkMatch[4] || '1', 10),
|
|
257
|
+
content: '',
|
|
258
|
+
};
|
|
259
|
+
hunkContent = [line];
|
|
260
|
+
lineIndex++;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Diff content lines
|
|
265
|
+
if (currentHunk) {
|
|
266
|
+
if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ') || line === '') {
|
|
267
|
+
hunkContent.push(line);
|
|
268
|
+
} else if (line.startsWith('diff --git')) {
|
|
269
|
+
// New file in multi-file diff - save current hunk
|
|
270
|
+
if (currentFile) {
|
|
271
|
+
currentHunk.content = hunkContent.join('\n');
|
|
272
|
+
hunks.push(currentHunk);
|
|
273
|
+
}
|
|
274
|
+
currentHunk = null;
|
|
275
|
+
hunkContent = [];
|
|
276
|
+
currentFile = null;
|
|
277
|
+
} else if (line.startsWith('index ') || line.startsWith('new file') || line.startsWith('deleted file')) {
|
|
278
|
+
// Git diff metadata - skip
|
|
279
|
+
} else if (line.trim() !== '') {
|
|
280
|
+
// Unexpected line in hunk
|
|
281
|
+
errors.push(`Unexpected line in diff at line ${lineIndex + 1}: "${line.slice(0, 50)}..."`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
lineIndex++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Save last hunk
|
|
289
|
+
if (currentHunk && currentFile) {
|
|
290
|
+
currentHunk.content = hunkContent.join('\n');
|
|
291
|
+
hunks.push(currentHunk);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate hunk line counts
|
|
295
|
+
for (const hunk of hunks) {
|
|
296
|
+
const hunkLines = hunk.content.split('\n').filter(l => !l.startsWith('@@'));
|
|
297
|
+
const addLines = hunkLines.filter(l => l.startsWith('+')).length;
|
|
298
|
+
const delLines = hunkLines.filter(l => l.startsWith('-')).length;
|
|
299
|
+
const ctxLines = hunkLines.filter(l => l.startsWith(' ') || l === '').length;
|
|
300
|
+
|
|
301
|
+
const expectedOld = delLines + ctxLines;
|
|
302
|
+
const expectedNew = addLines + ctxLines;
|
|
303
|
+
|
|
304
|
+
// Allow some tolerance for trailing newlines
|
|
305
|
+
if (Math.abs(expectedOld - hunk.oldLines) > 1) {
|
|
306
|
+
errors.push(`Hunk for ${hunk.file}: old line count mismatch (header: ${hunk.oldLines}, actual: ${expectedOld})`);
|
|
307
|
+
}
|
|
308
|
+
if (Math.abs(expectedNew - hunk.newLines) > 1) {
|
|
309
|
+
errors.push(`Hunk for ${hunk.file}: new line count mismatch (header: ${hunk.newLines}, actual: ${expectedNew})`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
valid: errors.length === 0,
|
|
315
|
+
errors,
|
|
316
|
+
hunks,
|
|
317
|
+
filesAffected,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// PATH SAFETY VALIDATION
|
|
323
|
+
// ============================================================================
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Validate that file paths are safe (no traversal, no system paths)
|
|
327
|
+
*/
|
|
328
|
+
export function validatePathSafety(paths: string[], projectRoot: string): { safe: boolean; issues: string[] } {
|
|
329
|
+
const issues: string[] = [];
|
|
330
|
+
|
|
331
|
+
for (const filePath of paths) {
|
|
332
|
+
// Check for unsafe patterns
|
|
333
|
+
for (const pattern of UNSAFE_PATH_PATTERNS) {
|
|
334
|
+
if (pattern.test(filePath)) {
|
|
335
|
+
issues.push(`Unsafe path pattern detected: ${filePath}`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Resolve and check if within project
|
|
341
|
+
const resolved = path.resolve(projectRoot, filePath);
|
|
342
|
+
const relative = path.relative(projectRoot, resolved);
|
|
343
|
+
|
|
344
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
345
|
+
issues.push(`Path escapes project root: ${filePath}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { safe: issues.length === 0, issues };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================================================
|
|
353
|
+
// COMMAND SAFETY VALIDATION
|
|
354
|
+
// ============================================================================
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Validate that commands are safe to run
|
|
358
|
+
*/
|
|
359
|
+
export function validateCommandSafety(commands: string[]): { safe: boolean; issues: string[] } {
|
|
360
|
+
const issues: string[] = [];
|
|
361
|
+
|
|
362
|
+
for (const cmd of commands) {
|
|
363
|
+
for (const pattern of UNSAFE_COMMANDS) {
|
|
364
|
+
if (pattern.test(cmd)) {
|
|
365
|
+
issues.push(`Potentially unsafe command: ${cmd}`);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { safe: issues.length === 0, issues };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============================================================================
|
|
375
|
+
// STUB DETECTION
|
|
376
|
+
// ============================================================================
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Detect placeholder/stub code in diff additions
|
|
380
|
+
*/
|
|
381
|
+
export function detectStubs(diff: string): { hasStubs: boolean; stubs: string[] } {
|
|
382
|
+
const stubs: string[] = [];
|
|
383
|
+
const lines = diff.split('\n');
|
|
384
|
+
|
|
385
|
+
for (const line of lines) {
|
|
386
|
+
// Only check added lines
|
|
387
|
+
if (!line.startsWith('+') || line.startsWith('+++')) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const content = line.slice(1); // Remove the + prefix
|
|
392
|
+
|
|
393
|
+
for (const pattern of STUB_PATTERNS) {
|
|
394
|
+
if (pattern.test(content)) {
|
|
395
|
+
stubs.push(content.trim().slice(0, 100));
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { hasStubs: stubs.length > 0, stubs };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// MAIN VALIDATOR
|
|
406
|
+
// ============================================================================
|
|
407
|
+
|
|
408
|
+
export interface FullValidationResult {
|
|
409
|
+
valid: boolean;
|
|
410
|
+
errors: string[];
|
|
411
|
+
warnings: string[];
|
|
412
|
+
output?: GuardrailV1Output;
|
|
413
|
+
diffValidation?: DiffValidationResult;
|
|
414
|
+
pathSafety?: { safe: boolean; issues: string[] };
|
|
415
|
+
commandSafety?: { safe: boolean; issues: string[] };
|
|
416
|
+
stubDetection?: { hasStubs: boolean; stubs: string[] };
|
|
417
|
+
wasMarkdownWrapped?: boolean;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Full validation pipeline for agent output
|
|
422
|
+
*/
|
|
423
|
+
export function validateAgentOutput(
|
|
424
|
+
raw: string,
|
|
425
|
+
projectRoot: string,
|
|
426
|
+
options?: { strictMarkdown?: boolean }
|
|
427
|
+
): FullValidationResult {
|
|
428
|
+
const errors: string[] = [];
|
|
429
|
+
const warnings: string[] = [];
|
|
430
|
+
|
|
431
|
+
// Step 1: Check for markdown fences and strip them (forgiving mode)
|
|
432
|
+
const wasMarkdownWrapped = isMarkdownWrapped(raw);
|
|
433
|
+
|
|
434
|
+
if (wasMarkdownWrapped) {
|
|
435
|
+
if (options?.strictMarkdown) {
|
|
436
|
+
// In strict mode, reject markdown-wrapped output entirely
|
|
437
|
+
return {
|
|
438
|
+
valid: false,
|
|
439
|
+
errors: ['Output must be raw JSON, not wrapped in markdown fences. Remove ```json and ``` markers.'],
|
|
440
|
+
warnings: [],
|
|
441
|
+
wasMarkdownWrapped: true,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// In forgiving mode, strip fences but warn
|
|
445
|
+
warnings.push('Output was wrapped in markdown fences (```json). Stripped automatically. AI should return raw JSON.');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const stripped = stripMarkdownFences(raw);
|
|
449
|
+
|
|
450
|
+
// Step 2: Parse JSON
|
|
451
|
+
let parsed: unknown;
|
|
452
|
+
try {
|
|
453
|
+
parsed = JSON.parse(stripped);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
return {
|
|
456
|
+
valid: false,
|
|
457
|
+
errors: [`Invalid JSON: ${(e as Error).message}`],
|
|
458
|
+
warnings: [],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Step 3: Validate JSON shape
|
|
463
|
+
const shapeResult = validateJsonShape(parsed);
|
|
464
|
+
if (!shapeResult.valid || !shapeResult.sanitized) {
|
|
465
|
+
return {
|
|
466
|
+
valid: false,
|
|
467
|
+
errors: shapeResult.errors,
|
|
468
|
+
warnings: shapeResult.warnings,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
warnings.push(...shapeResult.warnings);
|
|
472
|
+
const output = shapeResult.sanitized;
|
|
473
|
+
|
|
474
|
+
// Step 4: Validate unified diff
|
|
475
|
+
const diffValidation = validateUnifiedDiff(output.diff);
|
|
476
|
+
if (!diffValidation.valid) {
|
|
477
|
+
errors.push(...diffValidation.errors);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Step 5: Validate path safety
|
|
481
|
+
const pathSafety = validatePathSafety(diffValidation.filesAffected, projectRoot);
|
|
482
|
+
if (!pathSafety.safe) {
|
|
483
|
+
errors.push(...pathSafety.issues);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Step 6: Validate command safety
|
|
487
|
+
const commandSafety = validateCommandSafety(output.commands);
|
|
488
|
+
if (!commandSafety.safe) {
|
|
489
|
+
warnings.push(...commandSafety.issues); // Warn but don't block
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Step 7: Detect stubs
|
|
493
|
+
const stubDetection = detectStubs(output.diff);
|
|
494
|
+
if (stubDetection.hasStubs) {
|
|
495
|
+
errors.push(`Stub/placeholder code detected in diff: ${stubDetection.stubs.slice(0, 3).join(', ')}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
valid: errors.length === 0,
|
|
500
|
+
errors,
|
|
501
|
+
warnings,
|
|
502
|
+
output,
|
|
503
|
+
diffValidation,
|
|
504
|
+
pathSafety,
|
|
505
|
+
commandSafety,
|
|
506
|
+
stubDetection,
|
|
507
|
+
wasMarkdownWrapped,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Quick check if output is markdown-wrapped (for error messages)
|
|
513
|
+
*/
|
|
514
|
+
export function isMarkdownWrapped(raw: string): boolean {
|
|
515
|
+
const trimmed = raw.trim();
|
|
516
|
+
return trimmed.startsWith('```') || trimmed.includes('```json');
|
|
517
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified AutoFix Module - Public API
|
|
3
|
+
*
|
|
4
|
+
* Exports for the verified autofix pipeline:
|
|
5
|
+
* - Format validation
|
|
6
|
+
* - Temp workspace management
|
|
7
|
+
* - Repo fingerprinting
|
|
8
|
+
* - Full pipeline orchestration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Format validation
|
|
12
|
+
export {
|
|
13
|
+
validateAgentOutput,
|
|
14
|
+
validateJsonShape,
|
|
15
|
+
validateUnifiedDiff,
|
|
16
|
+
validatePathSafety,
|
|
17
|
+
validateCommandSafety,
|
|
18
|
+
detectStubs,
|
|
19
|
+
stripMarkdownFences,
|
|
20
|
+
isMarkdownWrapped,
|
|
21
|
+
type GuardrailV1Output,
|
|
22
|
+
type ValidationResult,
|
|
23
|
+
type DiffValidationResult,
|
|
24
|
+
type ParsedHunk,
|
|
25
|
+
type FullValidationResult,
|
|
26
|
+
} from './format-validator';
|
|
27
|
+
|
|
28
|
+
// Workspace management
|
|
29
|
+
export {
|
|
30
|
+
TempWorkspace,
|
|
31
|
+
tempWorkspace,
|
|
32
|
+
type WorkspaceOptions,
|
|
33
|
+
type WorkspaceInfo,
|
|
34
|
+
type ApplyResult,
|
|
35
|
+
type VerifyResult,
|
|
36
|
+
type CheckResult,
|
|
37
|
+
} from './workspace';
|
|
38
|
+
|
|
39
|
+
// Repo fingerprinting
|
|
40
|
+
export {
|
|
41
|
+
fingerprintRepo,
|
|
42
|
+
getInstallCommand,
|
|
43
|
+
getBuildCommand,
|
|
44
|
+
getTestCommand,
|
|
45
|
+
getTypecheckCommand,
|
|
46
|
+
type PackageManager,
|
|
47
|
+
type BuildTool,
|
|
48
|
+
type Framework,
|
|
49
|
+
type TestRunner,
|
|
50
|
+
type RepoFingerprint,
|
|
51
|
+
type FingerprintResult,
|
|
52
|
+
} from './repo-fingerprint';
|
|
53
|
+
|
|
54
|
+
// Pipeline orchestration
|
|
55
|
+
export {
|
|
56
|
+
VerifiedAutofixPipeline,
|
|
57
|
+
verifiedAutofixPipeline,
|
|
58
|
+
formatPipelineResult,
|
|
59
|
+
formatPipelineResultJson,
|
|
60
|
+
type PipelineOptions,
|
|
61
|
+
type PipelineStage,
|
|
62
|
+
type PipelineResult,
|
|
63
|
+
} from './pipeline';
|