@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,510 @@
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 } from './sourceSafe.js';
6
+
7
+ export const ACCEPTED_RISK_REGISTER_SCHEMA_VERSION = 'accepted_risk_register.v1';
8
+ export const ACCEPTED_RISK_STATE_SCHEMA_VERSION = 'ralph_accepted_risk.v1';
9
+ export const ACCEPTED_RISK_STEPPER_ID = 'ralph.accept';
10
+ export const ACCEPTED_RISK_STEPPER_VERSION = '1.0.0';
11
+ export const ACCEPTED_RISK_STATUSES = Object.freeze(['active', 'expired']);
12
+ export const ACCEPTED_RISK_REPORT_VISIBILITY = Object.freeze(['visible', 'summary_only']);
13
+ export const ACCEPTED_RISK_HIGH_RISK_SEVERITIES = Object.freeze(['critical', 'high']);
14
+
15
+ const REGISTER_FILE = '.vibesecur/accepted-risks.json';
16
+ const STATE_FILE = 'accepted-risk.json';
17
+ const SHA256 = /^[a-f0-9]{64}$/i;
18
+ const UUIDISH = /^[a-zA-Z0-9_.:-]+$/;
19
+ const SEVERITIES = new Set(['critical', 'high', 'medium', 'low']);
20
+ const HIGH_RISK = new Set(ACCEPTED_RISK_HIGH_RISK_SEVERITIES);
21
+ const nowIso = () => new Date().toISOString();
22
+
23
+ function stableSort(value) {
24
+ if (Array.isArray(value)) return value.map(stableSort);
25
+ if (!value || typeof value !== 'object') return value;
26
+ return Object.keys(value)
27
+ .sort()
28
+ .reduce((acc, key) => {
29
+ acc[key] = stableSort(value[key]);
30
+ return acc;
31
+ }, {});
32
+ }
33
+
34
+ function stableStringify(value) {
35
+ return JSON.stringify(stableSort(value));
36
+ }
37
+
38
+ function sha256(value) {
39
+ return crypto.createHash('sha256').update(value).digest('hex');
40
+ }
41
+
42
+ function requireString(value, name, { max = 500, pattern = null } = {}) {
43
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string`);
44
+ if (value.length > max) throw new Error(`${name} must be ${max} characters or fewer`);
45
+ if (pattern && !pattern.test(value)) throw new Error(`${name} has an invalid format`);
46
+ return value;
47
+ }
48
+
49
+ function optionalString(value, name, options) {
50
+ if (value === undefined || value === null) return null;
51
+ return requireString(value, name, options);
52
+ }
53
+
54
+ function requireIsoDate(value, name) {
55
+ requireString(value, name, { max: 40 });
56
+ if (Number.isNaN(Date.parse(value))) throw new Error(`${name} must be an ISO timestamp`);
57
+ return value;
58
+ }
59
+
60
+ function optionalIsoDate(value, name) {
61
+ if (value === undefined || value === null) return null;
62
+ return requireIsoDate(value, name);
63
+ }
64
+
65
+ function asArray(value, name) {
66
+ if (value === undefined) return [];
67
+ if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
68
+ return value;
69
+ }
70
+
71
+ function normalizeSeverity(value) {
72
+ const severity = String(value || 'medium').toLowerCase();
73
+ return SEVERITIES.has(severity) ? severity : 'medium';
74
+ }
75
+
76
+ function isHighRisk(severity) {
77
+ return HIGH_RISK.has(normalizeSeverity(severity));
78
+ }
79
+
80
+ // A single accepted-risk record is human-authored audit metadata, never source.
81
+ export function validateAcceptedRiskRecord(input = {}) {
82
+ assertSourceSafePayload(input, 'acceptedRisk.record');
83
+ const severity = normalizeSeverity(input.severity);
84
+ const findingKey = optionalString(input.findingKey, 'acceptedRisk.findingKey', { max: 64, pattern: SHA256 });
85
+ const findingId = optionalString(input.findingId, 'acceptedRisk.findingId', { max: 120, pattern: UUIDISH });
86
+ if (!findingKey && !findingId) {
87
+ throw new Error('acceptedRisk record requires findingKey or findingId to attach to a tracked finding');
88
+ }
89
+ const reportVisibility = optionalString(input.reportVisibility, 'acceptedRisk.reportVisibility', { max: 40 }) || 'visible';
90
+ if (!ACCEPTED_RISK_REPORT_VISIBILITY.includes(reportVisibility)) {
91
+ throw new Error(`acceptedRisk.reportVisibility must be one of ${ACCEPTED_RISK_REPORT_VISIBILITY.join(', ')} (accepted risk is never hidden)`);
92
+ }
93
+ const reason = optionalString(input.reason, 'acceptedRisk.reason', { max: 1000 });
94
+ const reviewer = optionalString(input.reviewer, 'acceptedRisk.reviewer', { max: 255 });
95
+ const acceptedAt = input.acceptedAt ? requireIsoDate(input.acceptedAt, 'acceptedRisk.acceptedAt') : nowIso();
96
+ const expiresAt = optionalIsoDate(input.expiresAt, 'acceptedRisk.expiresAt');
97
+ const reviewBy = optionalIsoDate(input.reviewBy, 'acceptedRisk.reviewBy');
98
+
99
+ if (isHighRisk(severity)) {
100
+ if (!reason) throw new Error('High-risk accepted risk requires an explicit reason');
101
+ if (!reviewer) throw new Error('High-risk accepted risk requires reviewer metadata');
102
+ if (!expiresAt) throw new Error('High-risk accepted risk requires an expiresAt review/expiry date');
103
+ }
104
+ if (expiresAt && Date.parse(expiresAt) <= Date.parse(acceptedAt)) {
105
+ throw new Error('acceptedRisk.expiresAt must be after acceptedAt');
106
+ }
107
+
108
+ const record = {
109
+ riskId: requireString(input.riskId, 'acceptedRisk.riskId', { max: 160, pattern: UUIDISH }),
110
+ findingKey,
111
+ findingId,
112
+ taskId: optionalString(input.taskId, 'acceptedRisk.taskId', { max: 120, pattern: UUIDISH }),
113
+ severity,
114
+ reason,
115
+ reviewer,
116
+ acceptedAt,
117
+ expiresAt,
118
+ reviewBy,
119
+ reportVisibility,
120
+ metadata: cloneSourceSafe(input.metadata || {}, 'acceptedRisk.metadata'),
121
+ };
122
+ assertSourceSafePayload(record, 'acceptedRisk.record');
123
+ return record;
124
+ }
125
+
126
+ export function validateAcceptedRiskRegister(input = {}) {
127
+ assertSourceSafePayload(input, 'acceptedRiskRegister');
128
+ const records = asArray(input.records, 'acceptedRiskRegister.records').map(validateAcceptedRiskRecord);
129
+ const riskIds = new Set();
130
+ for (const record of records) {
131
+ if (riskIds.has(record.riskId)) {
132
+ throw new Error(`acceptedRiskRegister has duplicate riskId ${record.riskId}`);
133
+ }
134
+ riskIds.add(record.riskId);
135
+ }
136
+ const register = {
137
+ schemaVersion: requireString(input.schemaVersion, 'acceptedRiskRegister.schemaVersion', { max: 80, pattern: UUIDISH }),
138
+ generatedAt: input.generatedAt ? requireIsoDate(input.generatedAt, 'acceptedRiskRegister.generatedAt') : nowIso(),
139
+ updatedAt: input.updatedAt ? requireIsoDate(input.updatedAt, 'acceptedRiskRegister.updatedAt') : nowIso(),
140
+ records,
141
+ privacy: cloneSourceSafe(input.privacy || {
142
+ rawSourceStored: false,
143
+ secretValuesCaptured: false,
144
+ complianceCertification: false,
145
+ artifactStorage: 'local_metadata_only',
146
+ }, 'acceptedRiskRegister.privacy'),
147
+ };
148
+ if (register.schemaVersion !== ACCEPTED_RISK_REGISTER_SCHEMA_VERSION) {
149
+ throw new Error(`acceptedRiskRegister.schemaVersion must be ${ACCEPTED_RISK_REGISTER_SCHEMA_VERSION}`);
150
+ }
151
+ return register;
152
+ }
153
+
154
+ function isExpired(record, generatedAt) {
155
+ if (!record.expiresAt) return false;
156
+ return Date.parse(record.expiresAt) <= Date.parse(generatedAt);
157
+ }
158
+
159
+ // Returns only ACTIVE (non-expired) accepted-risk records keyed for comparison precedence.
160
+ export function evaluateActiveAcceptedRisk({ register = {}, generatedAt = nowIso() } = {}) {
161
+ const safeRegister = validateAcceptedRiskRegister({
162
+ schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
163
+ ...register,
164
+ });
165
+ const byFindingKey = new Map();
166
+ const byFindingId = new Map();
167
+ const active = [];
168
+ for (const record of safeRegister.records) {
169
+ if (isExpired(record, generatedAt)) continue;
170
+ active.push(record);
171
+ if (record.findingKey && !byFindingKey.has(record.findingKey)) byFindingKey.set(record.findingKey, record);
172
+ if (record.findingId && !byFindingId.has(record.findingId)) byFindingId.set(record.findingId, record);
173
+ }
174
+ return { active, byFindingKey, byFindingId };
175
+ }
176
+
177
+ // Index the register for comparison precedence. `known*` cover every record
178
+ // (active or expired) so the register, when present, is authoritative over the
179
+ // legacy AgentFixTask acceptedRiskRef field. `active*` cover only non-expired
180
+ // records that should still mark a finding as accepted risk.
181
+ export function indexAcceptedRiskForComparison({ register = {}, generatedAt = nowIso() } = {}) {
182
+ const safeRegister = validateAcceptedRiskRegister({
183
+ schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
184
+ ...register,
185
+ });
186
+ const knownByKey = new Map();
187
+ const knownById = new Map();
188
+ const activeByKey = new Map();
189
+ const activeById = new Map();
190
+ for (const record of safeRegister.records) {
191
+ if (record.findingKey && !knownByKey.has(record.findingKey)) knownByKey.set(record.findingKey, record);
192
+ if (record.findingId && !knownById.has(record.findingId)) knownById.set(record.findingId, record);
193
+ if (isExpired(record, generatedAt)) continue;
194
+ if (record.findingKey && !activeByKey.has(record.findingKey)) activeByKey.set(record.findingKey, record);
195
+ if (record.findingId && !activeById.has(record.findingId)) activeById.set(record.findingId, record);
196
+ }
197
+ return { knownByKey, knownById, activeByKey, activeById };
198
+ }
199
+
200
+ export function buildAcceptedRiskStateArtifact({
201
+ register = {},
202
+ comparisonArtifact = null,
203
+ generatedAt = nowIso(),
204
+ sourceRefs = {},
205
+ } = {}) {
206
+ const safeRegister = validateAcceptedRiskRegister({
207
+ schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
208
+ ...register,
209
+ });
210
+ const at = requireIsoDate(generatedAt, 'acceptedRiskState.generatedAt');
211
+
212
+ // Build a quick lookup of unresolved comparison findings so we can flag
213
+ // accepted risks whose expiry has reverted a finding to unresolved.
214
+ const unresolvedStatuses = new Set(['new', 'worsened', 'unchanged', 'manualReview']);
215
+ const unresolvedByKey = new Set();
216
+ const unresolvedById = new Set();
217
+ if (comparisonArtifact && Array.isArray(comparisonArtifact.comparisons)) {
218
+ for (const row of comparisonArtifact.comparisons) {
219
+ if (!unresolvedStatuses.has(row.status)) continue;
220
+ if (row.findingKey) unresolvedByKey.add(row.findingKey);
221
+ const id = row.current?.id || row.previous?.id;
222
+ if (id) unresolvedById.add(id);
223
+ }
224
+ }
225
+
226
+ const records = safeRegister.records.map((record) => {
227
+ const expired = isExpired(record, at);
228
+ const stillUnresolved = (record.findingKey && unresolvedByKey.has(record.findingKey))
229
+ || (record.findingId && unresolvedById.has(record.findingId));
230
+ return {
231
+ ...record,
232
+ status: expired ? 'expired' : 'active',
233
+ appliedToComparison: !expired,
234
+ revertedToUnresolved: Boolean(expired && stillUnresolved),
235
+ };
236
+ }).sort((a, b) => a.riskId.localeCompare(b.riskId));
237
+
238
+ const activeRecords = records.filter((record) => record.status === 'active');
239
+ const expiredRecords = records.filter((record) => record.status === 'expired');
240
+
241
+ const artifact = {
242
+ schemaVersion: ACCEPTED_RISK_STATE_SCHEMA_VERSION,
243
+ generatedAt: at,
244
+ sources: {
245
+ acceptedRiskRegister: sourceRefs.acceptedRiskRegister || 'accepted_risk_register',
246
+ ralphComparison: sourceRefs.ralphComparison || null,
247
+ },
248
+ summary: {
249
+ totalRecords: records.length,
250
+ activeCount: activeRecords.length,
251
+ expiredCount: expiredRecords.length,
252
+ highRiskActiveCount: activeRecords.filter((record) => isHighRisk(record.severity)).length,
253
+ expiredRevertedCount: expiredRecords.filter((record) => record.revertedToUnresolved).length,
254
+ },
255
+ reportHandoff: {
256
+ acceptedRiskVisible: records.length > 0,
257
+ activeAcceptedRisk: activeRecords.map((record) => ({
258
+ riskId: record.riskId,
259
+ findingKey: record.findingKey,
260
+ findingId: record.findingId,
261
+ severity: record.severity,
262
+ reviewer: record.reviewer,
263
+ expiresAt: record.expiresAt,
264
+ reportVisibility: record.reportVisibility,
265
+ })),
266
+ expiredAcceptedRisk: expiredRecords.map((record) => ({
267
+ riskId: record.riskId,
268
+ findingKey: record.findingKey,
269
+ findingId: record.findingId,
270
+ severity: record.severity,
271
+ expiresAt: record.expiresAt,
272
+ revertedToUnresolved: record.revertedToUnresolved,
273
+ })),
274
+ note: 'Accepted risk is shown separately from resolved findings and never removes the underlying finding from history. Expired accepted risk reverts the finding to unresolved.',
275
+ },
276
+ records,
277
+ privacy: {
278
+ rawSourceStored: false,
279
+ secretValuesCaptured: false,
280
+ complianceCertification: false,
281
+ artifactStorage: 'local_metadata_only',
282
+ },
283
+ };
284
+ return validateAcceptedRiskStateArtifact(artifact);
285
+ }
286
+
287
+ function validateAcceptedRiskStateRecord(input = {}) {
288
+ const base = validateAcceptedRiskRecord(input);
289
+ const status = requireString(input.status, 'acceptedRiskState.record.status', { max: 40 });
290
+ if (!ACCEPTED_RISK_STATUSES.includes(status)) {
291
+ throw new Error(`acceptedRiskState.record.status must be one of ${ACCEPTED_RISK_STATUSES.join(', ')}`);
292
+ }
293
+ if (isHighRisk(base.severity) && !base.expiresAt) {
294
+ throw new Error('High-risk accepted-risk state records require expiresAt');
295
+ }
296
+ return {
297
+ ...base,
298
+ status,
299
+ appliedToComparison: Boolean(input.appliedToComparison),
300
+ revertedToUnresolved: Boolean(input.revertedToUnresolved),
301
+ };
302
+ }
303
+
304
+ export function validateAcceptedRiskStateArtifact(input = {}) {
305
+ assertSourceSafePayload(input, 'acceptedRiskStateArtifact');
306
+ const artifact = {
307
+ schemaVersion: requireString(input.schemaVersion, 'acceptedRiskState.schemaVersion', { max: 80, pattern: UUIDISH }),
308
+ generatedAt: requireIsoDate(input.generatedAt, 'acceptedRiskState.generatedAt'),
309
+ sources: cloneSourceSafe(input.sources || {}, 'acceptedRiskState.sources'),
310
+ summary: cloneSourceSafe(input.summary || {}, 'acceptedRiskState.summary'),
311
+ reportHandoff: cloneSourceSafe(input.reportHandoff || {}, 'acceptedRiskState.reportHandoff'),
312
+ records: asArray(input.records, 'acceptedRiskState.records').map(validateAcceptedRiskStateRecord),
313
+ privacy: cloneSourceSafe(input.privacy || {}, 'acceptedRiskState.privacy'),
314
+ };
315
+ if (artifact.schemaVersion !== ACCEPTED_RISK_STATE_SCHEMA_VERSION) {
316
+ throw new Error(`acceptedRiskState.schemaVersion must be ${ACCEPTED_RISK_STATE_SCHEMA_VERSION}`);
317
+ }
318
+ return artifact;
319
+ }
320
+
321
+ export function hashAcceptedRiskStateArtifact(artifact) {
322
+ assertSourceSafePayload(artifact, 'acceptedRiskStateArtifact');
323
+ return sha256(stableStringify(validateAcceptedRiskStateArtifact(artifact)));
324
+ }
325
+
326
+ export async function readAcceptedRiskRegister({ rootPath }) {
327
+ const target = path.join(path.resolve(rootPath), REGISTER_FILE);
328
+ try {
329
+ const raw = await fs.readFile(target, 'utf8');
330
+ const parsed = JSON.parse(raw);
331
+ return { register: validateAcceptedRiskRegister(parsed), raw, exists: true };
332
+ } catch (error) {
333
+ if (error.code === 'ENOENT') {
334
+ return {
335
+ register: validateAcceptedRiskRegister({ schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION, records: [] }),
336
+ raw: null,
337
+ exists: false,
338
+ };
339
+ }
340
+ throw error;
341
+ }
342
+ }
343
+
344
+ async function writeRegisterFile({ rootPath, register }) {
345
+ const safeRegister = validateAcceptedRiskRegister(register);
346
+ const relativeUri = REGISTER_FILE;
347
+ const target = path.join(path.resolve(rootPath), relativeUri);
348
+ await fs.mkdir(path.dirname(target), { recursive: true });
349
+ const serialized = `${JSON.stringify(stableSort(safeRegister), null, 2)}\n`;
350
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
351
+ await fs.writeFile(tmp, serialized, 'utf8');
352
+ await fs.rename(tmp, target);
353
+ return { uri: relativeUri, hash: sha256(serialized) };
354
+ }
355
+
356
+ // Human/CLI/MCP entrypoint: append or update one accepted-risk record by riskId.
357
+ export async function appendAcceptedRiskRecord({ rootPath, record }) {
358
+ const newRecord = validateAcceptedRiskRecord({
359
+ ...record,
360
+ riskId: record?.riskId || `risk_${crypto.randomUUID()}`,
361
+ });
362
+ const { register } = await readAcceptedRiskRegister({ rootPath });
363
+ const others = register.records.filter((existing) => existing.riskId !== newRecord.riskId);
364
+ const updated = validateAcceptedRiskRegister({
365
+ schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
366
+ generatedAt: register.generatedAt,
367
+ updatedAt: nowIso(),
368
+ records: [...others, newRecord],
369
+ privacy: register.privacy,
370
+ });
371
+ const written = await writeRegisterFile({ rootPath, register: updated });
372
+ return { record: newRecord, register: updated, ...written };
373
+ }
374
+
375
+ export async function writeAcceptedRiskStateArtifact({ rootPath, runId, artifact }) {
376
+ const safeArtifact = validateAcceptedRiskStateArtifact(artifact);
377
+ const safeRunId = validateRunIdSegment(runId, 'runId');
378
+ const relativeUri = `.vibesecur/deep-scans/${safeRunId}/${STATE_FILE}`;
379
+ const target = path.join(path.resolve(rootPath), relativeUri);
380
+ await fs.mkdir(path.dirname(target), { recursive: true });
381
+ const serialized = `${JSON.stringify(stableSort(safeArtifact), null, 2)}\n`;
382
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
383
+ await fs.writeFile(tmp, serialized, 'utf8');
384
+ await fs.rename(tmp, target);
385
+ return { uri: relativeUri, hash: sha256(serialized) };
386
+ }
387
+
388
+ async function readLocalArtifact({ rootPath, ref, type }) {
389
+ if (!ref || ref.type !== type || ref.storage !== 'local' || !ref.uri) {
390
+ throw new Error(`Accepted-risk tracking requires a local ${type} artifact reference`);
391
+ }
392
+ const resolvedRoot = path.resolve(rootPath);
393
+ const target = path.resolve(resolvedRoot, ref.uri);
394
+ const relative = path.relative(resolvedRoot, target);
395
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
396
+ throw new Error(`Accepted-risk artifact ref for ${type} escapes the bound root`);
397
+ }
398
+ const raw = await fs.readFile(target, 'utf8');
399
+ if (!ref.hash) {
400
+ throw new Error(`Accepted-risk tracking requires ${type} artifact refs to include a hash`);
401
+ }
402
+ const actualHash = sha256(raw);
403
+ if (actualHash.toLowerCase() !== String(ref.hash).toLowerCase()) {
404
+ throw new Error(`Accepted-risk ${type} artifact hash mismatch: expected ${ref.hash}, got ${actualHash}`);
405
+ }
406
+ const parsed = JSON.parse(raw);
407
+ assertSourceSafePayload(parsed, `acceptedRisk.${type}`);
408
+ return parsed;
409
+ }
410
+
411
+ function findArtifactRef(state, type, preferredKeys = []) {
412
+ const artifacts = state?.artifacts || {};
413
+ for (const key of preferredKeys) {
414
+ if (artifacts[key]?.type === type) return artifacts[key];
415
+ }
416
+ return Object.values(artifacts).find((ref) => ref?.type === type) || null;
417
+ }
418
+
419
+ export function ralphAcceptStepper() {
420
+ return {
421
+ id: ACCEPTED_RISK_STEPPER_ID,
422
+ version: ACCEPTED_RISK_STEPPER_VERSION,
423
+ title: 'Ralph Accepted Risk Stepper',
424
+ category: 'ralph_loop',
425
+ requiredInputs: [],
426
+ producedArtifacts: ['ralph_accepted_risk'],
427
+ defaultTimeoutMs: 30000,
428
+ async run({ runId, config = {}, state = {}, tools = {} }) {
429
+ const rootPath = tools.rootPath || config.rootPath || '.';
430
+ const startedAt = nowIso();
431
+ const { register, exists } = await readAcceptedRiskRegister({ rootPath });
432
+ if (!exists || register.records.length === 0) {
433
+ return {
434
+ stepperId: ACCEPTED_RISK_STEPPER_ID,
435
+ version: ACCEPTED_RISK_STEPPER_VERSION,
436
+ status: 'skipped',
437
+ startedAt,
438
+ finishedAt: nowIso(),
439
+ evidence: [],
440
+ findings: [],
441
+ artifacts: [],
442
+ receipts: [],
443
+ summary: 'Accepted-risk evaluation skipped because no accepted-risk register exists.',
444
+ skippedReason: 'No .vibesecur/accepted-risks.json register records present.',
445
+ nextActions: ['Record an accepted risk with deepScanAcceptRisk (MCP) or deep-scan-accept-risk (CLI) before relying on this artifact.'],
446
+ };
447
+ }
448
+
449
+ const comparisonRef = findArtifactRef(state, 'ralph_comparison', ['ralph.compare']);
450
+ const comparisonArtifact = comparisonRef
451
+ ? await readLocalArtifact({ rootPath, ref: comparisonRef, type: 'ralph_comparison' })
452
+ : null;
453
+
454
+ const generatedAt = config.generatedAt || startedAt;
455
+ const artifact = buildAcceptedRiskStateArtifact({
456
+ register,
457
+ comparisonArtifact,
458
+ generatedAt,
459
+ sourceRefs: {
460
+ acceptedRiskRegister: 'accepted_risk_register',
461
+ ralphComparison: comparisonRef?.id || null,
462
+ },
463
+ });
464
+ const contentHash = hashAcceptedRiskStateArtifact(artifact);
465
+ const written = await writeAcceptedRiskStateArtifact({ rootPath, runId, artifact });
466
+ const safeRunId = validateRunIdSegment(runId, 'runId');
467
+ const ref = createArtifactRef({
468
+ id: `artifact-${safeRunId}-ralph-accepted-risk`,
469
+ type: 'ralph_accepted_risk',
470
+ storage: 'local',
471
+ uri: written.uri,
472
+ hash: written.hash,
473
+ preview: 'Accepted-risk state with reason/reviewer/expiry metadata and active/expired status only.',
474
+ metadata: {
475
+ schemaVersion: ACCEPTED_RISK_STATE_SCHEMA_VERSION,
476
+ contentHash,
477
+ generatedBy: ACCEPTED_RISK_STEPPER_ID,
478
+ activeCount: artifact.summary.activeCount,
479
+ expiredCount: artifact.summary.expiredCount,
480
+ expiredRevertedCount: artifact.summary.expiredRevertedCount,
481
+ },
482
+ });
483
+
484
+ return {
485
+ stepperId: ACCEPTED_RISK_STEPPER_ID,
486
+ version: ACCEPTED_RISK_STEPPER_VERSION,
487
+ status: 'passed',
488
+ startedAt,
489
+ finishedAt: nowIso(),
490
+ evidence: [{
491
+ type: 'hash',
492
+ label: 'Accepted-risk state artifact hash',
493
+ hash: ref.hash,
494
+ preview: 'Accepted-risk state artifact written locally.',
495
+ summary: `${artifact.summary.activeCount} active, ${artifact.summary.expiredCount} expired accepted-risk record(s).`,
496
+ metadata: {
497
+ artifactId: ref.id,
498
+ contentHash,
499
+ ralphComparisonArtifactId: comparisonRef?.id || null,
500
+ },
501
+ }],
502
+ findings: [],
503
+ artifacts: [ref],
504
+ receipts: [],
505
+ summary: `Accepted-risk evaluation generated: ${artifact.summary.activeCount} active, ${artifact.summary.expiredCount} expired, ${artifact.summary.expiredRevertedCount} reverted to unresolved.`,
506
+ nextActions: ['Use accepted-risk state in reports/passports; expired accepted risk must be re-reviewed or fixed before release reliance.'],
507
+ };
508
+ },
509
+ };
510
+ }