@saiteja1123/mcp-server 1.1.4 → 1.1.6

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.
Files changed (37) hide show
  1. package/package.json +59 -55
  2. package/src/api-scan.mjs +362 -93
  3. package/src/cli.js +771 -322
  4. package/src/deep-scan/contracts.js +201 -0
  5. package/src/deep-scan/deterministic-scan.js +337 -0
  6. package/src/deep-scan/index.js +109 -0
  7. package/src/deep-scan/project-map.js +507 -0
  8. package/src/deep-scan/ralph-accept.js +510 -0
  9. package/src/deep-scan/ralph-compare.js +498 -0
  10. package/src/deep-scan/ralph-tasks.js +598 -0
  11. package/src/deep-scan/ralph-track.js +548 -0
  12. package/src/deep-scan/registry.js +159 -0
  13. package/src/deep-scan/runtime.js +275 -0
  14. package/src/deep-scan/sample-steppers.js +128 -0
  15. package/src/deep-scan/sourceSafe.js +73 -0
  16. package/src/deep-scan/status.js +70 -0
  17. package/src/deep-scan/store.js +57 -0
  18. package/src/deep-scan/test-plan.js +760 -0
  19. package/src/index.js +6 -5
  20. package/src/lock.mjs +55 -14
  21. package/src/mcp-config.mjs +161 -0
  22. package/src/middleware/governance.js +135 -0
  23. package/src/orchestrator/runScan.js +211 -0
  24. package/src/project-bindings.mjs +215 -0
  25. package/src/rule-engine/index.js +2 -1
  26. package/src/rule-engine/localScan.js +39 -12
  27. package/src/rule-engine/metadata.js +20 -0
  28. package/src/rule-engine/prompt.js +6 -5
  29. package/src/rule-engine/rules.js +71 -43
  30. package/src/rule-engine/score.js +5 -4
  31. package/src/security/pathGuard.js +170 -0
  32. package/src/selftest.js +2473 -0
  33. package/src/server.js +109 -150
  34. package/src/tools/deepScan.js +286 -0
  35. package/src/tools/localScan.js +85 -0
  36. package/src/tools/projects.js +124 -0
  37. package/src/tools/scanFile.js +131 -0
@@ -0,0 +1,598 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { createArtifactRef, validateRunIdSegment } from './contracts.js';
5
+ import { assertSourceSafePayload, cloneSourceSafe, findRawSourceField } from './sourceSafe.js';
6
+
7
+ export const AGENT_FIX_TASK_SCHEMA_VERSION = 'agent_fix_task.v1';
8
+ export const AGENT_FIX_TASK_STEPPER_ID = 'ralph.tasks';
9
+ export const AGENT_FIX_TASK_STEPPER_VERSION = '1.0.0';
10
+ export const AGENT_FIX_TASK_STATUSES = Object.freeze(['ready', 'manual', 'blocked']);
11
+
12
+ const TASKS_FILE = 'agent-fix-tasks.json';
13
+ const SHA256 = /^[a-f0-9]{64}$/i;
14
+ const UUIDISH = /^[a-zA-Z0-9_.:-]+$/;
15
+
16
+ const READY_CATEGORIES = new Set([
17
+ 'api_abuse',
18
+ 'auth_session',
19
+ 'deployment_config',
20
+ 'input_validation_xss',
21
+ 'injection',
22
+ 'path_traversal',
23
+ 'secrets_config',
24
+ 'ssrf',
25
+ ]);
26
+
27
+ const CATEGORY_TEMPLATES = Object.freeze({
28
+ secrets_config: {
29
+ label: 'Secret and configuration handling',
30
+ problem: 'A secret or configuration handling risk is linked to local evidence. Keep secret values outside source, prompts, logs, and Vibesecur artifacts.',
31
+ requiredBehavior: 'Move sensitive values to the local or deployment secret manager, leave only safe references in source, and preserve Vibesecur metadata-only artifacts.',
32
+ rollback: 'If the change breaks startup, restore the previous local configuration and retry with a secret-manager-backed reference.',
33
+ },
34
+ injection: {
35
+ label: 'Injection defense',
36
+ problem: 'A deterministic finding indicates untrusted input may reach an executable, query, or interpreter boundary.',
37
+ requiredBehavior: 'Constrain the input path with parameterization, allowlists, or safe framework APIs before execution or query construction.',
38
+ rollback: 'If behavior regresses, restore the previous call path and isolate the unsafe input boundary with a narrower test case.',
39
+ },
40
+ input_validation_xss: {
41
+ label: 'Input validation and XSS defense',
42
+ problem: 'A finding or test-plan candidate points to user-controlled data reaching a browser-rendered or HTML-sensitive surface.',
43
+ requiredBehavior: 'Encode or sanitize untrusted data at the correct boundary and keep framework escaping enabled.',
44
+ rollback: 'If rendering changes break expected UI behavior, restore the rendering path and add a focused escaping regression test first.',
45
+ },
46
+ ssrf: {
47
+ label: 'Outbound request control',
48
+ problem: 'A finding or test-plan candidate points to user-influenced outbound network requests.',
49
+ requiredBehavior: 'Restrict outbound targets with a fixed allowlist, reject private or metadata-network destinations, and keep redirects bounded.',
50
+ rollback: 'If legitimate integrations fail, restore the previous integration path and add explicit allowed destinations one at a time.',
51
+ },
52
+ path_traversal: {
53
+ label: 'Filesystem path containment',
54
+ problem: 'A finding or test-plan candidate indicates a path may escape the intended project or storage root.',
55
+ requiredBehavior: 'Resolve paths against the intended root, reject traversal or absolute escape attempts, and keep reads/writes inside the bound directory.',
56
+ rollback: 'If legitimate file access fails, restore the previous path and add a focused containment test for the intended safe directory.',
57
+ },
58
+ api_abuse: {
59
+ label: 'API abuse and authorization control',
60
+ problem: 'A route-like surface needs explicit abuse, authorization, validation, or quota coverage.',
61
+ requiredBehavior: 'Keep authentication, authorization, validation, rate limits, and quota checks fail-closed before privileged behavior runs.',
62
+ rollback: 'If a legitimate route flow breaks, restore the prior behavior and reintroduce the control behind a narrower route-level test.',
63
+ },
64
+ auth_session: {
65
+ label: 'Auth and session review',
66
+ problem: 'Auth or session behavior requires local review with credentials, cookies, or tokens that must not be copied into artifacts.',
67
+ requiredBehavior: 'Verify login, logout, session refresh, token expiry, and unauthorized access locally without exposing credential values.',
68
+ rollback: 'If auth flow changes lock out legitimate users, revert the local change and capture a redacted failure summary for human review.',
69
+ },
70
+ deployment_config: {
71
+ label: 'Deployment configuration review',
72
+ problem: 'Deployment metadata needs review for secret handling, security headers, and runtime configuration boundaries.',
73
+ requiredBehavior: 'Keep deployment secrets referenced through the host secret manager and preserve security controls in runtime config.',
74
+ rollback: 'If deployment config changes fail, restore the prior deployment file and add one configuration control at a time.',
75
+ },
76
+ manual_review: {
77
+ label: 'Manual security review',
78
+ problem: 'This category is not yet covered by a deterministic Ralph template and requires manual review by a human in the local project.',
79
+ requiredBehavior: 'Inspect the linked local files and evidence, define the smallest safe fix or acceptance decision, and keep values out of Vibesecur artifacts.',
80
+ rollback: 'If the review cannot produce a safe local action, leave the task manual and require human approval before release reliance.',
81
+ },
82
+ });
83
+
84
+ const nowIso = () => new Date().toISOString();
85
+
86
+ function stableSort(value) {
87
+ if (Array.isArray(value)) return value.map(stableSort);
88
+ if (!value || typeof value !== 'object') return value;
89
+ return Object.keys(value)
90
+ .sort()
91
+ .reduce((acc, key) => {
92
+ acc[key] = stableSort(value[key]);
93
+ return acc;
94
+ }, {});
95
+ }
96
+
97
+ function stableStringify(value) {
98
+ return JSON.stringify(stableSort(value));
99
+ }
100
+
101
+ function sha256(value) {
102
+ return crypto.createHash('sha256').update(value).digest('hex');
103
+ }
104
+
105
+ function requireString(value, name, { max = 500, pattern = null } = {}) {
106
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string`);
107
+ if (value.length > max) throw new Error(`${name} must be ${max} characters or fewer`);
108
+ if (pattern && !pattern.test(value)) throw new Error(`${name} has an invalid format`);
109
+ return value;
110
+ }
111
+
112
+ function optionalString(value, name, options) {
113
+ if (value === undefined || value === null) return null;
114
+ return requireString(value, name, options);
115
+ }
116
+
117
+ function requireIsoDate(value, name) {
118
+ requireString(value, name, { max: 40 });
119
+ if (Number.isNaN(Date.parse(value))) throw new Error(`${name} must be an ISO timestamp`);
120
+ return value;
121
+ }
122
+
123
+ function asArray(value, name) {
124
+ if (value === undefined) return [];
125
+ if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
126
+ return value;
127
+ }
128
+
129
+ function asObject(value, name) {
130
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
131
+ throw new Error(`${name} must be an object`);
132
+ }
133
+ return value;
134
+ }
135
+
136
+ function safeText(value, fallback, max = 300) {
137
+ const text = String(value || '').replace(/\s+/g, ' ').trim().slice(0, max);
138
+ if (!text) return fallback;
139
+ if (findRawSourceField(text, 'agentFixTask.text')) return fallback;
140
+ return text;
141
+ }
142
+
143
+ function safeIdSegment(value, fallback = 'item') {
144
+ const token = String(value || '')
145
+ .toLowerCase()
146
+ .replace(/[^a-z0-9_.:-]+/g, '_')
147
+ .replace(/^_+|_+$/g, '')
148
+ .slice(0, 80);
149
+ return token || fallback;
150
+ }
151
+
152
+ function normalizeSeverity(value) {
153
+ const severity = safeIdSegment(value || 'medium', 'medium');
154
+ return ['critical', 'high', 'medium', 'low'].includes(severity) ? severity : 'medium';
155
+ }
156
+
157
+ function countsBy(items, key) {
158
+ return items.reduce((acc, item) => {
159
+ const value = item[key] || 'unknown';
160
+ acc[value] = (acc[value] || 0) + 1;
161
+ return acc;
162
+ }, {});
163
+ }
164
+
165
+ function deterministicFindingMap(deterministicScan = {}) {
166
+ return new Map((deterministicScan.findings || []).map((finding) => [finding.id, finding]));
167
+ }
168
+
169
+ function selectTemplate(category) {
170
+ if (READY_CATEGORIES.has(category)) return CATEGORY_TEMPLATES[category];
171
+ return CATEGORY_TEMPLATES.manual_review;
172
+ }
173
+
174
+ function candidateStatus(candidate, category) {
175
+ if (!READY_CATEGORIES.has(category)) return 'manual';
176
+ if (candidate.status === 'blocked') return 'blocked';
177
+ if (candidate.status === 'manual') return 'manual';
178
+ return 'ready';
179
+ }
180
+
181
+ function statusReason(candidate, status, category) {
182
+ if (status === 'blocked') {
183
+ return safeText(candidate.blockedReason, 'This task is blocked until the missing local harness, dependency, or environment is supplied.', 400);
184
+ }
185
+ if (status === 'manual' && !READY_CATEGORIES.has(category)) {
186
+ return 'No deterministic Ralph template exists for this category yet; human local review is required.';
187
+ }
188
+ if (status === 'manual') {
189
+ return safeText(candidate.manualReason, 'Manual local review is required before release reliance.', 400);
190
+ }
191
+ return null;
192
+ }
193
+
194
+ function taskIdFor(candidate) {
195
+ return `fix-${sha256(candidate.dedupeKey || candidate.id || candidate.title).slice(0, 16)}`;
196
+ }
197
+
198
+ function targetFilesFor(candidate) {
199
+ const files = asArray(candidate.targetSurface?.files, 'testCase.targetSurface.files')
200
+ .map((file, index) => requireString(file, `testCase.targetSurface.files[${index}]`, { max: 300 }));
201
+ return files.length > 0 ? files : ['local-target-requires-review'];
202
+ }
203
+
204
+ function artifactRefsFor(candidate, sourceRefs) {
205
+ return [...new Set([
206
+ sourceRefs.securityTestPlan,
207
+ ...asArray(candidate.sourceArtifactRefs, 'testCase.sourceArtifactRefs'),
208
+ ].filter(Boolean))];
209
+ }
210
+
211
+ function sourceFindingsFor(candidate, findingById) {
212
+ return asArray(candidate.sourceFindingIds, 'testCase.sourceFindingIds')
213
+ .map((findingId) => {
214
+ const finding = findingById.get(findingId);
215
+ if (!finding) {
216
+ throw new Error(`agentFixTask.sourceFindingIds references unknown deterministic finding ${findingId}`);
217
+ }
218
+ return finding;
219
+ });
220
+ }
221
+
222
+ function verificationCommand({ candidate, status }) {
223
+ if (status === 'blocked') return null;
224
+ if (candidate.suggestedCommand) return safeText(candidate.suggestedCommand, 'Run the linked local test command.', 300);
225
+ if (status === 'manual') {
226
+ return 'Manual local review with redacted notes; then rerun Vibesecur MCP for the bound root.';
227
+ }
228
+ return 'Create or identify a focused local regression test, run it, then rerun Vibesecur MCP for the bound root.';
229
+ }
230
+
231
+ function buildInstructions({ candidate, template, status, targetFiles }) {
232
+ const title = safeText(candidate.title, 'security task', 180);
233
+ const fileScope = targetFiles.join(', ');
234
+ const verification = verificationCommand({ candidate, status });
235
+ const rerun = 'Rerun Vibesecur MCP with `deepScanStart` or `scanSummary` on the bound root and compare this task against the linked artifact refs.';
236
+ const acceptedRiskPath = 'If the risk cannot be fixed now, require human approval and record an accepted-risk reference before release reliance.';
237
+ return {
238
+ problem: `${template.problem} Candidate: ${title}.`,
239
+ likelyFileScope: `Inspect only the linked local target files: ${fileScope}.`,
240
+ requiredBehavior: template.requiredBehavior,
241
+ verification: verification || 'Supply the missing local harness or environment, then run the focused verification command.',
242
+ rerun,
243
+ rollback: template.rollback,
244
+ acceptedRiskPath,
245
+ localFirst: 'Do not paste source bodies, credentials, cookies, provider keys, install tokens, JWTs, or env values into Vibesecur artifacts or prompts.',
246
+ };
247
+ }
248
+
249
+ function makeTask({ candidate, findingById, sourceRefs }) {
250
+ const category = safeIdSegment(candidate.category, 'manual_review');
251
+ const template = selectTemplate(category);
252
+ const status = candidateStatus(candidate, category);
253
+ const findingIds = asArray(candidate.sourceFindingIds, 'testCase.sourceFindingIds')
254
+ .map((id, index) => requireString(id, `testCase.sourceFindingIds[${index}]`, { max: 160, pattern: UUIDISH }));
255
+ const testPlanCandidateIds = [requireString(candidate.id, 'testCase.id', { max: 120, pattern: UUIDISH })];
256
+ const targetFiles = targetFilesFor(candidate);
257
+ const artifactRefs = artifactRefsFor(candidate, sourceRefs);
258
+ const relatedFindings = sourceFindingsFor(candidate, findingById);
259
+ const risk = normalizeSeverity(candidate.priority || candidate.risk || relatedFindings[0]?.severity);
260
+ const instructions = buildInstructions({ candidate, template, status, targetFiles });
261
+ const command = verificationCommand({ candidate, status });
262
+ const reason = statusReason(candidate, status, category);
263
+ const task = {
264
+ taskId: taskIdFor(candidate),
265
+ version: AGENT_FIX_TASK_SCHEMA_VERSION,
266
+ title: safeText(candidate.title, `${template.label} task`, 220),
267
+ category: READY_CATEGORIES.has(category) ? category : 'manual_review',
268
+ status,
269
+ safeSummary: safeText(
270
+ candidate.purpose,
271
+ `${template.label} task generated from source-safe Test Plan evidence.`,
272
+ 500,
273
+ ),
274
+ findingIds,
275
+ testPlanCandidateIds,
276
+ artifactRefs,
277
+ targetFiles,
278
+ instructions,
279
+ verification: {
280
+ command,
281
+ expectedOutcome: 'Local verification runs or manual review is recorded, then Vibesecur rerun evidence no longer shows the linked unresolved risk unless human acceptance is recorded.',
282
+ rerunVibesecur: 'Use the bound-root MCP Deep Scan or scan summary path after local changes; do not rely on edit claims alone.',
283
+ },
284
+ risk: {
285
+ severity: risk,
286
+ priority: normalizeSeverity(candidate.priority || risk),
287
+ category: READY_CATEGORIES.has(category) ? category : 'manual_review',
288
+ },
289
+ acceptedRiskRef: null,
290
+ blockedReason: status === 'blocked' ? reason : null,
291
+ manualReason: status === 'manual' ? reason : null,
292
+ metadata: {
293
+ generatedFrom: AGENT_FIX_TASK_STEPPER_ID,
294
+ template: READY_CATEGORIES.has(category) ? category : 'manual_review',
295
+ sourceStatus: candidate.status || null,
296
+ sourceRank: Number.isInteger(candidate.rank) ? candidate.rank : null,
297
+ sourceDedupeKey: safeText(candidate.dedupeKey, candidate.id, 240),
298
+ sourceFindingCount: findingIds.length,
299
+ },
300
+ };
301
+ if (task.artifactRefs.length === 0) throw new Error('agentFixTask.artifactRefs must include source artifact refs');
302
+ if (task.findingIds.length === 0 && task.testPlanCandidateIds.length === 0) {
303
+ throw new Error('agentFixTask must reference findings or test-plan items');
304
+ }
305
+ assertSourceSafePayload(task, 'agentFixTask');
306
+ return task;
307
+ }
308
+
309
+ function buildSources({ securityTestPlan = {}, deterministicScan = {}, sourceRefs }) {
310
+ return {
311
+ securityTestPlan: {
312
+ artifactRef: sourceRefs.securityTestPlan,
313
+ schemaVersion: securityTestPlan.schemaVersion || null,
314
+ totalTests: securityTestPlan.summary?.totalTests || 0,
315
+ downstreamContract: cloneSourceSafe(securityTestPlan.downstreamContract || {}, 'agentFixTask.sources.testPlanContract'),
316
+ },
317
+ deterministicScan: {
318
+ artifactRef: sourceRefs.deterministicScan,
319
+ schemaVersion: deterministicScan.schemaVersion || null,
320
+ findingCount: (deterministicScan.findings || []).length,
321
+ },
322
+ };
323
+ }
324
+
325
+ export function buildAgentFixTaskArtifact({
326
+ securityTestPlan,
327
+ deterministicScan = {},
328
+ generatedAt = nowIso(),
329
+ sourceRefs = {},
330
+ } = {}) {
331
+ const safeTestPlan = cloneSourceSafe(securityTestPlan || {}, 'agentFixTask.securityTestPlanInput');
332
+ const safeDeterministicScan = cloneSourceSafe(deterministicScan || {}, 'agentFixTask.deterministicScanInput');
333
+ const refs = {
334
+ securityTestPlan: sourceRefs.securityTestPlan || 'security_test_plan',
335
+ deterministicScan: sourceRefs.deterministicScan || 'deterministic_scan',
336
+ };
337
+ const findingById = deterministicFindingMap(safeDeterministicScan);
338
+ const tasks = asArray(safeTestPlan.testCases, 'securityTestPlan.testCases')
339
+ .map((candidate) => makeTask({ candidate, findingById, sourceRefs: refs }));
340
+ const artifact = {
341
+ schemaVersion: AGENT_FIX_TASK_SCHEMA_VERSION,
342
+ generatedAt: requireIsoDate(generatedAt, 'agentFixTask.generatedAt'),
343
+ root: {
344
+ name: safeTestPlan.root?.name || safeDeterministicScan.root?.name || 'project',
345
+ pathHash: safeTestPlan.root?.pathHash || safeDeterministicScan.root?.pathHash || null,
346
+ },
347
+ sources: buildSources({
348
+ securityTestPlan: safeTestPlan,
349
+ deterministicScan: safeDeterministicScan,
350
+ sourceRefs: refs,
351
+ }),
352
+ summary: {
353
+ totalTasks: tasks.length,
354
+ byStatus: countsBy(tasks, 'status'),
355
+ byCategory: countsBy(tasks, 'category'),
356
+ readyCount: tasks.filter((task) => task.status === 'ready').length,
357
+ manualCount: tasks.filter((task) => task.status === 'manual').length,
358
+ blockedCount: tasks.filter((task) => task.status === 'blocked').length,
359
+ highOrCriticalCount: tasks.filter((task) => ['critical', 'high'].includes(task.risk.severity)).length,
360
+ },
361
+ templates: {
362
+ version: AGENT_FIX_TASK_SCHEMA_VERSION,
363
+ categories: Object.keys(CATEGORY_TEMPLATES).sort(),
364
+ unknownCategoryFallback: 'manual_review',
365
+ },
366
+ tasks,
367
+ privacy: {
368
+ rawSourceStored: false,
369
+ secretValuesCaptured: false,
370
+ instructionsAreTemplates: true,
371
+ artifactStorage: 'local_metadata_and_hash_refs',
372
+ },
373
+ };
374
+ return validateAgentFixTaskArtifact(artifact);
375
+ }
376
+
377
+ function validateInstructions(input = {}) {
378
+ const instructions = {
379
+ problem: requireString(input.problem, 'agentFixTask.instructions.problem', { max: 700 }),
380
+ likelyFileScope: requireString(input.likelyFileScope, 'agentFixTask.instructions.likelyFileScope', { max: 500 }),
381
+ requiredBehavior: requireString(input.requiredBehavior, 'agentFixTask.instructions.requiredBehavior', { max: 700 }),
382
+ verification: requireString(input.verification, 'agentFixTask.instructions.verification', { max: 700 }),
383
+ rerun: requireString(input.rerun, 'agentFixTask.instructions.rerun', { max: 700 }),
384
+ rollback: requireString(input.rollback, 'agentFixTask.instructions.rollback', { max: 700 }),
385
+ acceptedRiskPath: requireString(input.acceptedRiskPath, 'agentFixTask.instructions.acceptedRiskPath', { max: 700 }),
386
+ localFirst: requireString(input.localFirst, 'agentFixTask.instructions.localFirst', { max: 700 }),
387
+ };
388
+ assertSourceSafePayload(instructions, 'agentFixTask.instructions');
389
+ return instructions;
390
+ }
391
+
392
+ function validateTask(input = {}) {
393
+ assertSourceSafePayload(input, 'agentFixTask');
394
+ const status = requireString(input.status, 'agentFixTask.status', { max: 40, pattern: UUIDISH });
395
+ if (!AGENT_FIX_TASK_STATUSES.includes(status)) {
396
+ throw new Error(`agentFixTask.status must be one of ${AGENT_FIX_TASK_STATUSES.join(', ')}`);
397
+ }
398
+ const task = {
399
+ taskId: requireString(input.taskId, 'agentFixTask.taskId', { max: 120, pattern: UUIDISH }),
400
+ version: requireString(input.version, 'agentFixTask.version', { max: 80, pattern: UUIDISH }),
401
+ title: requireString(input.title, 'agentFixTask.title', { max: 220 }),
402
+ category: requireString(input.category, 'agentFixTask.category', { max: 80, pattern: UUIDISH }),
403
+ status,
404
+ safeSummary: requireString(input.safeSummary, 'agentFixTask.safeSummary', { max: 500 }),
405
+ findingIds: asArray(input.findingIds, 'agentFixTask.findingIds')
406
+ .map((id, index) => requireString(id, `agentFixTask.findingIds[${index}]`, { max: 160, pattern: UUIDISH })),
407
+ testPlanCandidateIds: asArray(input.testPlanCandidateIds, 'agentFixTask.testPlanCandidateIds')
408
+ .map((id, index) => requireString(id, `agentFixTask.testPlanCandidateIds[${index}]`, { max: 160, pattern: UUIDISH })),
409
+ artifactRefs: asArray(input.artifactRefs, 'agentFixTask.artifactRefs')
410
+ .map((id, index) => requireString(id, `agentFixTask.artifactRefs[${index}]`, { max: 160, pattern: UUIDISH })),
411
+ targetFiles: asArray(input.targetFiles, 'agentFixTask.targetFiles')
412
+ .map((file, index) => requireString(file, `agentFixTask.targetFiles[${index}]`, { max: 300 })),
413
+ instructions: validateInstructions(input.instructions || {}),
414
+ verification: {
415
+ command: optionalString(input.verification?.command, 'agentFixTask.verification.command', { max: 500 }),
416
+ expectedOutcome: requireString(input.verification?.expectedOutcome, 'agentFixTask.verification.expectedOutcome', { max: 700 }),
417
+ rerunVibesecur: requireString(input.verification?.rerunVibesecur, 'agentFixTask.verification.rerunVibesecur', { max: 700 }),
418
+ },
419
+ risk: {
420
+ severity: normalizeSeverity(input.risk?.severity),
421
+ priority: normalizeSeverity(input.risk?.priority),
422
+ category: requireString(input.risk?.category || input.category, 'agentFixTask.risk.category', { max: 80, pattern: UUIDISH }),
423
+ },
424
+ acceptedRiskRef: optionalString(input.acceptedRiskRef, 'agentFixTask.acceptedRiskRef', { max: 160, pattern: UUIDISH }),
425
+ blockedReason: optionalString(input.blockedReason, 'agentFixTask.blockedReason', { max: 500 }),
426
+ manualReason: optionalString(input.manualReason, 'agentFixTask.manualReason', { max: 500 }),
427
+ metadata: cloneSourceSafe(input.metadata || {}, 'agentFixTask.metadata'),
428
+ };
429
+ if (task.version !== AGENT_FIX_TASK_SCHEMA_VERSION) {
430
+ throw new Error(`agentFixTask.version must be ${AGENT_FIX_TASK_SCHEMA_VERSION}`);
431
+ }
432
+ if (task.findingIds.length === 0 && task.testPlanCandidateIds.length === 0) {
433
+ throw new Error('agentFixTask must reference findings or test-plan items');
434
+ }
435
+ if (task.artifactRefs.length === 0) throw new Error('agentFixTask.artifactRefs must include source artifact refs');
436
+ if (task.targetFiles.length === 0) throw new Error('agentFixTask.targetFiles must include at least one file or local target');
437
+ if (status === 'blocked' && !task.blockedReason) throw new Error('blocked Agent Fix Tasks require blockedReason');
438
+ if (status === 'manual' && !task.manualReason) throw new Error('manual Agent Fix Tasks require manualReason');
439
+ assertSourceSafePayload(task, 'agentFixTask');
440
+ return task;
441
+ }
442
+
443
+ export function validateAgentFixTaskArtifact(input = {}) {
444
+ assertSourceSafePayload(input, 'agentFixTaskArtifact');
445
+ const artifact = {
446
+ schemaVersion: requireString(input.schemaVersion, 'agentFixTask.schemaVersion', { max: 80, pattern: UUIDISH }),
447
+ generatedAt: requireIsoDate(input.generatedAt, 'agentFixTask.generatedAt'),
448
+ root: {
449
+ name: requireString(input.root?.name || 'project', 'agentFixTask.root.name', { max: 180 }),
450
+ pathHash: optionalString(input.root?.pathHash, 'agentFixTask.root.pathHash', { max: 64, pattern: SHA256 }),
451
+ },
452
+ sources: cloneSourceSafe(asObject(input.sources || {}, 'agentFixTask.sources'), 'agentFixTask.sources'),
453
+ summary: cloneSourceSafe(asObject(input.summary || {}, 'agentFixTask.summary'), 'agentFixTask.summary'),
454
+ templates: cloneSourceSafe(asObject(input.templates || {}, 'agentFixTask.templates'), 'agentFixTask.templates'),
455
+ tasks: asArray(input.tasks, 'agentFixTask.tasks').map(validateTask),
456
+ privacy: cloneSourceSafe(asObject(input.privacy || {}, 'agentFixTask.privacy'), 'agentFixTask.privacy'),
457
+ };
458
+ if (artifact.schemaVersion !== AGENT_FIX_TASK_SCHEMA_VERSION) {
459
+ throw new Error(`agentFixTask.schemaVersion must be ${AGENT_FIX_TASK_SCHEMA_VERSION}`);
460
+ }
461
+ assertSourceSafePayload(artifact, 'agentFixTaskArtifact');
462
+ return artifact;
463
+ }
464
+
465
+ export function hashAgentFixTaskArtifact(artifact) {
466
+ assertSourceSafePayload(artifact, 'agentFixTaskArtifact');
467
+ return sha256(stableStringify(validateAgentFixTaskArtifact(artifact)));
468
+ }
469
+
470
+ async function readLocalArtifact({ rootPath, ref, type }) {
471
+ if (!ref || ref.type !== type || ref.storage !== 'local' || !ref.uri) {
472
+ throw new Error(`Agent Fix Task generation requires a local ${type} artifact reference`);
473
+ }
474
+ const resolvedRoot = path.resolve(rootPath);
475
+ const target = path.resolve(resolvedRoot, ref.uri);
476
+ const relative = path.relative(resolvedRoot, target);
477
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
478
+ throw new Error(`Agent Fix Task artifact ref for ${type} escapes the bound root`);
479
+ }
480
+ const raw = await fs.readFile(target, 'utf8');
481
+ if (!ref.hash) {
482
+ throw new Error(`Agent Fix Task generation requires ${type} artifact refs to include a hash`);
483
+ }
484
+ const actualHash = sha256(raw);
485
+ if (actualHash.toLowerCase() !== String(ref.hash).toLowerCase()) {
486
+ throw new Error(`Agent Fix Task ${type} artifact hash mismatch: expected ${ref.hash}, got ${actualHash}`);
487
+ }
488
+ const parsed = JSON.parse(raw);
489
+ assertSourceSafePayload(parsed, `agentFixTask.${type}`);
490
+ return parsed;
491
+ }
492
+
493
+ function findArtifactRef(state, type, preferredKeys = []) {
494
+ const artifacts = state?.artifacts || {};
495
+ for (const key of preferredKeys) {
496
+ if (artifacts[key]?.type === type) return artifacts[key];
497
+ }
498
+ return Object.values(artifacts).find((ref) => ref?.type === type) || null;
499
+ }
500
+
501
+ export async function writeAgentFixTaskArtifact({ rootPath, runId, artifact }) {
502
+ const safeArtifact = validateAgentFixTaskArtifact(artifact);
503
+ const safeRunId = validateRunIdSegment(runId, 'runId');
504
+ const relativeUri = `.vibesecur/deep-scans/${safeRunId}/${TASKS_FILE}`;
505
+ const target = path.join(path.resolve(rootPath), relativeUri);
506
+ await fs.mkdir(path.dirname(target), { recursive: true });
507
+ const serialized = `${JSON.stringify(stableSort(safeArtifact), null, 2)}\n`;
508
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
509
+ await fs.writeFile(tmp, serialized, 'utf8');
510
+ await fs.rename(tmp, target);
511
+ return {
512
+ uri: relativeUri,
513
+ hash: sha256(serialized),
514
+ };
515
+ }
516
+
517
+ export function ralphTaskStepper() {
518
+ return {
519
+ id: AGENT_FIX_TASK_STEPPER_ID,
520
+ version: AGENT_FIX_TASK_STEPPER_VERSION,
521
+ title: 'Ralph Agent Fix Task Stepper',
522
+ category: 'ralph_loop',
523
+ requiredInputs: ['security_test_plan', 'deterministic_scan'],
524
+ producedArtifacts: ['agent_fix_tasks'],
525
+ defaultTimeoutMs: 30000,
526
+ async run({ runId, config = {}, state = {}, tools = {} }) {
527
+ const rootPath = tools.rootPath || config.rootPath || '.';
528
+ const startedAt = nowIso();
529
+ const testPlanRef = findArtifactRef(state, 'security_test_plan', ['tests.plan']);
530
+ const deterministicScanRef = findArtifactRef(state, 'deterministic_scan', ['rules.scan']);
531
+ const securityTestPlan = await readLocalArtifact({
532
+ rootPath,
533
+ ref: testPlanRef,
534
+ type: 'security_test_plan',
535
+ });
536
+ const deterministicScan = await readLocalArtifact({
537
+ rootPath,
538
+ ref: deterministicScanRef,
539
+ type: 'deterministic_scan',
540
+ });
541
+ const artifact = buildAgentFixTaskArtifact({
542
+ securityTestPlan,
543
+ deterministicScan,
544
+ generatedAt: config.generatedAt || startedAt,
545
+ sourceRefs: {
546
+ securityTestPlan: testPlanRef.id,
547
+ deterministicScan: deterministicScanRef.id,
548
+ },
549
+ });
550
+ const contentHash = hashAgentFixTaskArtifact(artifact);
551
+ const written = await writeAgentFixTaskArtifact({ rootPath, runId, artifact });
552
+ const safeRunId = validateRunIdSegment(runId, 'runId');
553
+ const ref = createArtifactRef({
554
+ id: `artifact-${safeRunId}-agent-fix-tasks`,
555
+ type: 'agent_fix_tasks',
556
+ storage: 'local',
557
+ uri: written.uri,
558
+ hash: written.hash,
559
+ preview: 'Ralph Agent Fix Tasks: source-safe repair instructions, verification hints, and accepted-risk paths only.',
560
+ metadata: {
561
+ schemaVersion: AGENT_FIX_TASK_SCHEMA_VERSION,
562
+ contentHash,
563
+ generatedBy: AGENT_FIX_TASK_STEPPER_ID,
564
+ totalTasks: artifact.summary.totalTasks,
565
+ readyCount: artifact.summary.readyCount,
566
+ manualCount: artifact.summary.manualCount,
567
+ blockedCount: artifact.summary.blockedCount,
568
+ },
569
+ });
570
+
571
+ return {
572
+ stepperId: AGENT_FIX_TASK_STEPPER_ID,
573
+ version: AGENT_FIX_TASK_STEPPER_VERSION,
574
+ status: 'passed',
575
+ startedAt,
576
+ finishedAt: nowIso(),
577
+ evidence: [{
578
+ type: 'hash',
579
+ label: 'Agent Fix Task artifact hash',
580
+ hash: ref.hash,
581
+ preview: 'Source-safe Ralph task artifact written locally.',
582
+ summary: `${artifact.summary.totalTasks} task(s), ${artifact.summary.readyCount} ready for local agent work.`,
583
+ metadata: {
584
+ artifactId: ref.id,
585
+ contentHash,
586
+ securityTestPlanArtifactId: testPlanRef.id,
587
+ deterministicScanArtifactId: deterministicScanRef.id,
588
+ },
589
+ }],
590
+ findings: [],
591
+ artifacts: [ref],
592
+ receipts: [],
593
+ summary: `Ralph Agent Fix Tasks generated: ${artifact.summary.totalTasks} task(s), ${artifact.summary.readyCount} ready, ${artifact.summary.blockedCount} blocked, ${artifact.summary.manualCount} manual.`,
594
+ nextActions: ['Give Agent Fix Tasks to the local coding agent, then rerun Vibesecur for verification evidence.'],
595
+ };
596
+ },
597
+ };
598
+ }