@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.
- package/package.json +59 -55
- package/src/api-scan.mjs +362 -93
- package/src/cli.js +771 -322
- package/src/deep-scan/contracts.js +201 -0
- package/src/deep-scan/deterministic-scan.js +337 -0
- package/src/deep-scan/index.js +109 -0
- package/src/deep-scan/project-map.js +507 -0
- package/src/deep-scan/ralph-accept.js +510 -0
- package/src/deep-scan/ralph-compare.js +498 -0
- package/src/deep-scan/ralph-tasks.js +598 -0
- package/src/deep-scan/ralph-track.js +548 -0
- package/src/deep-scan/registry.js +159 -0
- package/src/deep-scan/runtime.js +275 -0
- package/src/deep-scan/sample-steppers.js +128 -0
- package/src/deep-scan/sourceSafe.js +73 -0
- package/src/deep-scan/status.js +70 -0
- package/src/deep-scan/store.js +57 -0
- package/src/deep-scan/test-plan.js +760 -0
- package/src/index.js +6 -5
- package/src/lock.mjs +55 -14
- package/src/mcp-config.mjs +161 -0
- package/src/middleware/governance.js +135 -0
- package/src/orchestrator/runScan.js +211 -0
- package/src/project-bindings.mjs +215 -0
- package/src/rule-engine/index.js +2 -1
- package/src/rule-engine/localScan.js +39 -12
- package/src/rule-engine/metadata.js +20 -0
- package/src/rule-engine/prompt.js +6 -5
- package/src/rule-engine/rules.js +71 -43
- package/src/rule-engine/score.js +5 -4
- package/src/security/pathGuard.js +170 -0
- package/src/selftest.js +2473 -0
- package/src/server.js +109 -150
- package/src/tools/deepScan.js +286 -0
- package/src/tools/localScan.js +85 -0
- package/src/tools/projects.js +124 -0
- package/src/tools/scanFile.js +131 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { assertSourceSafePayload, cloneSourceSafe } from './sourceSafe.js';
|
|
2
|
+
|
|
3
|
+
const ID_PATTERN = /^[a-zA-Z0-9_.:-]+$/;
|
|
4
|
+
|
|
5
|
+
const asString = (value, name, max = 160) => {
|
|
6
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string`);
|
|
7
|
+
if (value.length > max) throw new Error(`${name} must be ${max} characters or fewer`);
|
|
8
|
+
return value;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const validateDefinition = (definition) => {
|
|
12
|
+
const id = asString(definition?.id, 'stepper.id');
|
|
13
|
+
if (!ID_PATTERN.test(id)) throw new Error(`stepper.id has invalid characters: ${id}`);
|
|
14
|
+
const version = asString(definition?.version, 'stepper.version', 80);
|
|
15
|
+
if (typeof definition.run !== 'function') throw new Error(`stepper ${id} must provide a run() function`);
|
|
16
|
+
const normalized = {
|
|
17
|
+
id,
|
|
18
|
+
version,
|
|
19
|
+
title: asString(definition.title || id, 'stepper.title', 200),
|
|
20
|
+
category: asString(definition.category || 'general', 'stepper.category', 80),
|
|
21
|
+
requiredInputs: Array.isArray(definition.requiredInputs) ? [...definition.requiredInputs] : [],
|
|
22
|
+
producedArtifacts: Array.isArray(definition.producedArtifacts) ? [...definition.producedArtifacts] : [],
|
|
23
|
+
defaultTimeoutMs: Number.isFinite(Number(definition.defaultTimeoutMs))
|
|
24
|
+
? Number(definition.defaultTimeoutMs)
|
|
25
|
+
: 30000,
|
|
26
|
+
requiresApproval: Array.isArray(definition.requiresApproval) ? [...definition.requiresApproval] : [],
|
|
27
|
+
unsafeExternal: definition.unsafeExternal === true,
|
|
28
|
+
run: definition.run,
|
|
29
|
+
};
|
|
30
|
+
assertSourceSafePayload({
|
|
31
|
+
...normalized,
|
|
32
|
+
run: '[function]',
|
|
33
|
+
}, `stepper.${id}`);
|
|
34
|
+
return normalized;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class StepperRegistry {
|
|
38
|
+
constructor(definitions = []) {
|
|
39
|
+
this.definitions = new Map();
|
|
40
|
+
definitions.forEach((definition) => this.register(definition));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
register(definition) {
|
|
44
|
+
const normalized = validateDefinition(definition);
|
|
45
|
+
if (this.definitions.has(normalized.id)) {
|
|
46
|
+
throw new Error(`Duplicate stepper id registered: ${normalized.id}`);
|
|
47
|
+
}
|
|
48
|
+
this.definitions.set(normalized.id, normalized);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
unregister(id) {
|
|
53
|
+
this.definitions.delete(id);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get(id) {
|
|
58
|
+
return this.definitions.get(id) || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
list() {
|
|
62
|
+
return [...this.definitions.values()].map(({ run, ...definition }) => ({ ...definition }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
validateProfile(profile = {}) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
const normalizedSteps = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const steppers = Array.isArray(profile.steppers) ? profile.steppers : [];
|
|
70
|
+
|
|
71
|
+
if (!profile.id || typeof profile.id !== 'string') {
|
|
72
|
+
errors.push({ code: 'PROFILE_ID_REQUIRED', message: 'GraphProfile requires a string id.' });
|
|
73
|
+
}
|
|
74
|
+
if (!profile.version || typeof profile.version !== 'string') {
|
|
75
|
+
errors.push({ code: 'PROFILE_VERSION_REQUIRED', message: 'GraphProfile requires a string version.' });
|
|
76
|
+
}
|
|
77
|
+
if (steppers.length === 0) {
|
|
78
|
+
errors.push({ code: 'PROFILE_STEPPERS_REQUIRED', message: 'GraphProfile requires at least one stepper.' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
steppers.forEach((rawStep, index) => {
|
|
82
|
+
const step = cloneSourceSafe(rawStep || {}, `graphProfile.steppers[${index}]`);
|
|
83
|
+
const id = step.id;
|
|
84
|
+
if (!id || typeof id !== 'string') {
|
|
85
|
+
errors.push({ code: 'STEPPER_ID_REQUIRED', message: `Stepper at index ${index} requires an id.` });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (seen.has(id)) {
|
|
89
|
+
errors.push({ code: 'DUPLICATE_PROFILE_STEPPER', message: `GraphProfile repeats stepper id ${id}.` });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
seen.add(id);
|
|
93
|
+
|
|
94
|
+
const definition = this.get(id);
|
|
95
|
+
const enabled = step.enabled !== false;
|
|
96
|
+
const required = step.required !== false;
|
|
97
|
+
if (!definition && !enabled && !required) {
|
|
98
|
+
normalizedSteps.push({
|
|
99
|
+
id,
|
|
100
|
+
enabled,
|
|
101
|
+
required,
|
|
102
|
+
config: cloneSourceSafe(step.config || {}, `graphProfile.steppers[${index}].config`),
|
|
103
|
+
definition: {
|
|
104
|
+
id,
|
|
105
|
+
version: 'disabled',
|
|
106
|
+
requiresApproval: [],
|
|
107
|
+
unsafeExternal: false,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!definition) {
|
|
113
|
+
errors.push({ code: 'STEPPER_NOT_REGISTERED', message: `Stepper id ${id} is not registered.` });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!enabled && required) {
|
|
118
|
+
errors.push({ code: 'REQUIRED_STEPPER_DISABLED', message: `Required stepper ${id} cannot be disabled.` });
|
|
119
|
+
}
|
|
120
|
+
if (enabled && definition.unsafeExternal && definition.requiresApproval.length === 0) {
|
|
121
|
+
errors.push({
|
|
122
|
+
code: 'UNSAFE_EXTERNAL_MISSING_APPROVAL_REQUIREMENTS',
|
|
123
|
+
message: `Unsafe external stepper ${id} must define concrete approval requirements.`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
enabled
|
|
128
|
+
&& definition.unsafeExternal
|
|
129
|
+
&& step.requiresApproval !== true
|
|
130
|
+
&& profile.humanReview?.allowExternalTests !== true
|
|
131
|
+
) {
|
|
132
|
+
errors.push({
|
|
133
|
+
code: 'UNSAFE_EXTERNAL_REQUIRES_APPROVAL',
|
|
134
|
+
message: `Stepper ${id} requires explicit approval before external execution.`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
normalizedSteps.push({
|
|
139
|
+
id,
|
|
140
|
+
enabled,
|
|
141
|
+
required,
|
|
142
|
+
config: cloneSourceSafe(step.config || {}, `graphProfile.steppers[${index}].config`),
|
|
143
|
+
definition,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
valid: errors.length === 0,
|
|
149
|
+
errors,
|
|
150
|
+
steps: normalizedSteps,
|
|
151
|
+
profile: {
|
|
152
|
+
id: profile.id || '',
|
|
153
|
+
version: profile.version || '',
|
|
154
|
+
checkpoint: profile.checkpoint !== false,
|
|
155
|
+
humanReview: cloneSourceSafe(profile.humanReview || {}, 'graphProfile.humanReview'),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
validateApproval,
|
|
4
|
+
validateStepResult,
|
|
5
|
+
} from './contracts.js';
|
|
6
|
+
import { assertSourceSafePayload, cloneSourceSafe } from './sourceSafe.js';
|
|
7
|
+
|
|
8
|
+
const nowIso = () => new Date().toISOString();
|
|
9
|
+
|
|
10
|
+
const terminalForResume = new Set(['passed', 'skipped']);
|
|
11
|
+
const stopStatuses = new Set(['failed', 'blocked', 'needs_human']);
|
|
12
|
+
|
|
13
|
+
const hasApproved = (state, requirementId) =>
|
|
14
|
+
(state.approvals || []).some((approval) =>
|
|
15
|
+
approval.requirementId === requirementId && approval.decision === 'approved');
|
|
16
|
+
|
|
17
|
+
const hasDenied = (state, requirementId) =>
|
|
18
|
+
(state.approvals || []).some((approval) =>
|
|
19
|
+
approval.requirementId === requirementId && approval.decision === 'denied');
|
|
20
|
+
|
|
21
|
+
const hasRequested = (state, requirementId) =>
|
|
22
|
+
(state.approvals || []).some((approval) =>
|
|
23
|
+
approval.requirementId === requirementId && approval.decision === 'requested');
|
|
24
|
+
|
|
25
|
+
const makeStepResult = ({
|
|
26
|
+
stepperId,
|
|
27
|
+
version,
|
|
28
|
+
status,
|
|
29
|
+
summary,
|
|
30
|
+
skippedReason,
|
|
31
|
+
blockedReason,
|
|
32
|
+
needsHumanReason,
|
|
33
|
+
nextActions = [],
|
|
34
|
+
}) => {
|
|
35
|
+
const startedAt = nowIso();
|
|
36
|
+
return validateStepResult({
|
|
37
|
+
stepperId,
|
|
38
|
+
version,
|
|
39
|
+
status,
|
|
40
|
+
startedAt,
|
|
41
|
+
finishedAt: nowIso(),
|
|
42
|
+
evidence: [],
|
|
43
|
+
findings: [],
|
|
44
|
+
artifacts: [],
|
|
45
|
+
receipts: [],
|
|
46
|
+
summary,
|
|
47
|
+
nextActions,
|
|
48
|
+
skippedReason,
|
|
49
|
+
blockedReason,
|
|
50
|
+
needsHumanReason,
|
|
51
|
+
}, { stepperId, version });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export class DeepScanRuntime {
|
|
55
|
+
constructor({ registry, store, idFactory = () => `deep_scan_${crypto.randomUUID()}` }) {
|
|
56
|
+
if (!registry) throw new Error('DeepScanRuntime requires a registry');
|
|
57
|
+
if (!store) throw new Error('DeepScanRuntime requires a store');
|
|
58
|
+
this.registry = registry;
|
|
59
|
+
this.store = store;
|
|
60
|
+
this.idFactory = idFactory;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async createRun({
|
|
64
|
+
projectId,
|
|
65
|
+
projectHash = null,
|
|
66
|
+
graphProfile,
|
|
67
|
+
metadata = {},
|
|
68
|
+
}) {
|
|
69
|
+
assertSourceSafePayload({ graphProfile, metadata }, 'deepScanRun.create');
|
|
70
|
+
const validation = this.registry.validateProfile(graphProfile);
|
|
71
|
+
if (!validation.valid) {
|
|
72
|
+
const message = validation.errors.map((error) => error.message).join('; ');
|
|
73
|
+
throw new Error(`Invalid GraphProfile: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
const timestamp = nowIso();
|
|
76
|
+
const state = {
|
|
77
|
+
runId: this.idFactory(),
|
|
78
|
+
projectId,
|
|
79
|
+
projectHash,
|
|
80
|
+
graphProfileId: graphProfile.id,
|
|
81
|
+
graphProfileVersion: graphProfile.version,
|
|
82
|
+
status: 'pending',
|
|
83
|
+
graphProfile: cloneSourceSafe(graphProfile, 'deepScanRun.graphProfile'),
|
|
84
|
+
metadata: cloneSourceSafe(metadata, 'deepScanRun.metadata'),
|
|
85
|
+
artifacts: {},
|
|
86
|
+
findings: [],
|
|
87
|
+
receipts: [],
|
|
88
|
+
approvals: [],
|
|
89
|
+
stepResults: {},
|
|
90
|
+
createdAt: timestamp,
|
|
91
|
+
updatedAt: timestamp,
|
|
92
|
+
};
|
|
93
|
+
await this.store.saveRun(state);
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async startRun(runId) {
|
|
98
|
+
const state = await this.store.getRun(runId);
|
|
99
|
+
if (state.status !== 'pending') return this.resumeRun(runId);
|
|
100
|
+
return this.#execute(state);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async resumeRun(runId) {
|
|
104
|
+
const state = await this.store.getRun(runId);
|
|
105
|
+
return this.#execute(state);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async recordApproval({
|
|
109
|
+
runId,
|
|
110
|
+
requirementId,
|
|
111
|
+
decision,
|
|
112
|
+
approvedBy,
|
|
113
|
+
reason,
|
|
114
|
+
relatedFindingId,
|
|
115
|
+
relatedArtifactId,
|
|
116
|
+
metadata = {},
|
|
117
|
+
}) {
|
|
118
|
+
const state = await this.store.getRun(runId);
|
|
119
|
+
const approval = validateApproval({
|
|
120
|
+
id: `approval_${crypto.randomUUID()}`,
|
|
121
|
+
requirementId,
|
|
122
|
+
decision,
|
|
123
|
+
approvedBy,
|
|
124
|
+
reason,
|
|
125
|
+
relatedFindingId,
|
|
126
|
+
relatedArtifactId,
|
|
127
|
+
metadata,
|
|
128
|
+
createdAt: nowIso(),
|
|
129
|
+
});
|
|
130
|
+
state.approvals = [...(state.approvals || []), approval];
|
|
131
|
+
state.updatedAt = nowIso();
|
|
132
|
+
await this.store.saveRun(state);
|
|
133
|
+
return approval;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async #execute(state) {
|
|
137
|
+
const validation = this.registry.validateProfile(state.graphProfile);
|
|
138
|
+
if (!validation.valid) {
|
|
139
|
+
throw new Error(`Invalid GraphProfile: ${validation.errors.map((error) => error.message).join('; ')}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
state.status = 'running';
|
|
143
|
+
state.updatedAt = nowIso();
|
|
144
|
+
await this.store.saveRun(state);
|
|
145
|
+
|
|
146
|
+
for (const step of validation.steps) {
|
|
147
|
+
const existing = state.stepResults[step.id];
|
|
148
|
+
if (existing && terminalForResume.has(existing.status)) continue;
|
|
149
|
+
if (existing && stopStatuses.has(existing.status)) {
|
|
150
|
+
const requirements = step.definition.requiresApproval || [];
|
|
151
|
+
const deniedRequirement = requirements.find((requirementId) => hasDenied(state, requirementId));
|
|
152
|
+
if (existing.status === 'needs_human' && deniedRequirement) {
|
|
153
|
+
const blocked = makeStepResult({
|
|
154
|
+
stepperId: step.id,
|
|
155
|
+
version: step.definition.version,
|
|
156
|
+
status: 'blocked',
|
|
157
|
+
summary: `Stepper ${step.id} was blocked because approval ${deniedRequirement} was denied.`,
|
|
158
|
+
blockedReason: `Approval denied for ${deniedRequirement}.`,
|
|
159
|
+
nextActions: ['Resolve the denied approval or create a new run with a safe graph profile.'],
|
|
160
|
+
});
|
|
161
|
+
this.#applyStepResult(state, blocked);
|
|
162
|
+
state.status = 'blocked';
|
|
163
|
+
state.updatedAt = nowIso();
|
|
164
|
+
await this.store.saveRun(state);
|
|
165
|
+
return state;
|
|
166
|
+
}
|
|
167
|
+
const approvalsSatisfied = requirements.every((requirementId) => hasApproved(state, requirementId));
|
|
168
|
+
if (existing.status === 'needs_human' && approvalsSatisfied) {
|
|
169
|
+
delete state.stepResults[step.id];
|
|
170
|
+
} else {
|
|
171
|
+
state.status = existing.status;
|
|
172
|
+
state.updatedAt = nowIso();
|
|
173
|
+
await this.store.saveRun(state);
|
|
174
|
+
return state;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!step.enabled) {
|
|
179
|
+
const skipped = makeStepResult({
|
|
180
|
+
stepperId: step.id,
|
|
181
|
+
version: step.definition.version,
|
|
182
|
+
status: 'skipped',
|
|
183
|
+
summary: `Optional stepper ${step.id} is disabled by the active GraphProfile.`,
|
|
184
|
+
skippedReason: 'Disabled optional stepper.',
|
|
185
|
+
nextActions: ['Continue to the next enabled stepper.'],
|
|
186
|
+
});
|
|
187
|
+
this.#applyStepResult(state, skipped);
|
|
188
|
+
await this.store.saveRun(state);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const unmetApproval = (step.definition.requiresApproval || [])
|
|
193
|
+
.find((requirementId) => !hasApproved(state, requirementId));
|
|
194
|
+
if (unmetApproval) {
|
|
195
|
+
if (hasDenied(state, unmetApproval)) {
|
|
196
|
+
const blocked = makeStepResult({
|
|
197
|
+
stepperId: step.id,
|
|
198
|
+
version: step.definition.version,
|
|
199
|
+
status: 'blocked',
|
|
200
|
+
summary: `Stepper ${step.id} was blocked because approval ${unmetApproval} was denied.`,
|
|
201
|
+
blockedReason: `Approval denied for ${unmetApproval}.`,
|
|
202
|
+
nextActions: ['Resolve the denied approval or create a new run with a safe graph profile.'],
|
|
203
|
+
});
|
|
204
|
+
this.#applyStepResult(state, blocked);
|
|
205
|
+
state.status = 'blocked';
|
|
206
|
+
state.updatedAt = nowIso();
|
|
207
|
+
await this.store.saveRun(state);
|
|
208
|
+
return state;
|
|
209
|
+
}
|
|
210
|
+
if (!hasRequested(state, unmetApproval)) {
|
|
211
|
+
state.approvals.push(validateApproval({
|
|
212
|
+
id: `approval_${crypto.randomUUID()}`,
|
|
213
|
+
requirementId: unmetApproval,
|
|
214
|
+
decision: 'requested',
|
|
215
|
+
createdAt: nowIso(),
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
const needsHuman = makeStepResult({
|
|
219
|
+
stepperId: step.id,
|
|
220
|
+
version: step.definition.version,
|
|
221
|
+
status: 'needs_human',
|
|
222
|
+
summary: `Stepper ${step.id} requires human approval before it can continue.`,
|
|
223
|
+
needsHumanReason: `Approval required for ${unmetApproval}.`,
|
|
224
|
+
nextActions: [`Record approval for ${unmetApproval}, then resume this run.`],
|
|
225
|
+
});
|
|
226
|
+
this.#applyStepResult(state, needsHuman);
|
|
227
|
+
state.status = 'needs_human';
|
|
228
|
+
state.updatedAt = nowIso();
|
|
229
|
+
await this.store.saveRun(state);
|
|
230
|
+
return state;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = validateStepResult(await step.definition.run({
|
|
234
|
+
runId: state.runId,
|
|
235
|
+
projectId: state.projectId,
|
|
236
|
+
state: cloneSourceSafe(state, 'stepperInput.state'),
|
|
237
|
+
config: step.config,
|
|
238
|
+
policyPack: state.graphProfile.policyPack || {},
|
|
239
|
+
tools: {
|
|
240
|
+
rootPath: this.store.rootPath,
|
|
241
|
+
artifactBaseDir: this.store.baseDir,
|
|
242
|
+
},
|
|
243
|
+
}), {
|
|
244
|
+
stepperId: step.id,
|
|
245
|
+
version: step.definition.version,
|
|
246
|
+
});
|
|
247
|
+
this.#applyStepResult(state, result);
|
|
248
|
+
state.updatedAt = nowIso();
|
|
249
|
+
await this.store.saveRun(state);
|
|
250
|
+
|
|
251
|
+
if (step.required && stopStatuses.has(result.status)) {
|
|
252
|
+
state.status = result.status;
|
|
253
|
+
state.updatedAt = nowIso();
|
|
254
|
+
await this.store.saveRun(state);
|
|
255
|
+
return state;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
state.status = 'passed';
|
|
260
|
+
state.updatedAt = nowIso();
|
|
261
|
+
await this.store.saveRun(state);
|
|
262
|
+
return state;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#applyStepResult(state, result) {
|
|
266
|
+
state.stepResults[result.stepperId] = result;
|
|
267
|
+
for (const artifact of result.artifacts || []) {
|
|
268
|
+
state.artifacts[result.stepperId] = artifact;
|
|
269
|
+
state.artifacts[artifact.id] = artifact;
|
|
270
|
+
}
|
|
271
|
+
state.findings = [...(state.findings || []), ...(result.findings || [])];
|
|
272
|
+
state.receipts = [...(state.receipts || []), ...(result.receipts || [])];
|
|
273
|
+
state.status = result.status === 'passed' || result.status === 'skipped' ? 'running' : result.status;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createArtifactRef } from './contracts.js';
|
|
2
|
+
import { deterministicScanStepper } from './deterministic-scan.js';
|
|
3
|
+
import { ralphComparisonStepper } from './ralph-compare.js';
|
|
4
|
+
import { ralphFailureTrackStepper } from './ralph-track.js';
|
|
5
|
+
import { ralphAcceptStepper } from './ralph-accept.js';
|
|
6
|
+
import { projectMapStepper } from './project-map.js';
|
|
7
|
+
import { ralphTaskStepper } from './ralph-tasks.js';
|
|
8
|
+
import { securityTestPlanStepper } from './test-plan.js';
|
|
9
|
+
|
|
10
|
+
const now = () => new Date().toISOString();
|
|
11
|
+
|
|
12
|
+
export function sampleNoopStepper() {
|
|
13
|
+
return {
|
|
14
|
+
id: 'sample.noop',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
title: 'No-op Stepper',
|
|
17
|
+
category: 'runtime_proof',
|
|
18
|
+
requiredInputs: [],
|
|
19
|
+
producedArtifacts: [],
|
|
20
|
+
defaultTimeoutMs: 5000,
|
|
21
|
+
async run({ runId }) {
|
|
22
|
+
const startedAt = now();
|
|
23
|
+
return {
|
|
24
|
+
stepperId: 'sample.noop',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
status: 'passed',
|
|
27
|
+
startedAt,
|
|
28
|
+
finishedAt: now(),
|
|
29
|
+
evidence: [{
|
|
30
|
+
type: 'runtime',
|
|
31
|
+
label: 'No-op execution',
|
|
32
|
+
preview: `Run ${runId} executed the no-op stepper.`,
|
|
33
|
+
}],
|
|
34
|
+
findings: [],
|
|
35
|
+
artifacts: [],
|
|
36
|
+
summary: 'No-op stepper executed successfully.',
|
|
37
|
+
nextActions: ['Continue to the next Deep Scan stepper.'],
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sampleArtifactStepper() {
|
|
44
|
+
return {
|
|
45
|
+
id: 'sample.artifact',
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
title: 'Sample Artifact Stepper',
|
|
48
|
+
category: 'runtime_proof',
|
|
49
|
+
requiredInputs: [],
|
|
50
|
+
producedArtifacts: ['runtime.sample_artifact'],
|
|
51
|
+
defaultTimeoutMs: 5000,
|
|
52
|
+
async run({ runId }) {
|
|
53
|
+
const startedAt = now();
|
|
54
|
+
const artifact = createArtifactRef({
|
|
55
|
+
id: `artifact-${runId}-sample`,
|
|
56
|
+
type: 'runtime_sample',
|
|
57
|
+
storage: 'local',
|
|
58
|
+
uri: `.vibesecur/deep-scans/${runId}/sample-artifact.json`,
|
|
59
|
+
hash: 'd'.repeat(64),
|
|
60
|
+
preview: 'Synthetic metadata-only runtime proof artifact.',
|
|
61
|
+
metadata: { generatedBy: 'sample.artifact' },
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
stepperId: 'sample.artifact',
|
|
65
|
+
version: '1.0.0',
|
|
66
|
+
status: 'passed',
|
|
67
|
+
startedAt,
|
|
68
|
+
finishedAt: now(),
|
|
69
|
+
evidence: [{
|
|
70
|
+
type: 'hash',
|
|
71
|
+
label: 'Sample artifact hash',
|
|
72
|
+
hash: artifact.hash,
|
|
73
|
+
preview: 'Metadata-only artifact reference created.',
|
|
74
|
+
}],
|
|
75
|
+
findings: [],
|
|
76
|
+
artifacts: [artifact],
|
|
77
|
+
summary: 'Sample artifact reference created.',
|
|
78
|
+
nextActions: ['Inspect Deep Scan status.'],
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function sampleNeedsHumanStepper() {
|
|
85
|
+
return {
|
|
86
|
+
id: 'sample.needsHuman',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
title: 'Human Approval Stepper',
|
|
89
|
+
category: 'runtime_proof',
|
|
90
|
+
requiredInputs: [],
|
|
91
|
+
producedArtifacts: [],
|
|
92
|
+
defaultTimeoutMs: 5000,
|
|
93
|
+
requiresApproval: ['sample.needsHuman'],
|
|
94
|
+
async run() {
|
|
95
|
+
const startedAt = now();
|
|
96
|
+
return {
|
|
97
|
+
stepperId: 'sample.needsHuman',
|
|
98
|
+
version: '1.0.0',
|
|
99
|
+
status: 'passed',
|
|
100
|
+
startedAt,
|
|
101
|
+
finishedAt: now(),
|
|
102
|
+
evidence: [{
|
|
103
|
+
type: 'approval',
|
|
104
|
+
label: 'Human approval recorded',
|
|
105
|
+
preview: 'Approval metadata was present before the step ran.',
|
|
106
|
+
}],
|
|
107
|
+
findings: [],
|
|
108
|
+
artifacts: [],
|
|
109
|
+
summary: 'Human approval requirement satisfied.',
|
|
110
|
+
nextActions: ['Continue to the next Deep Scan stepper.'],
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createSampleStepperRegistry(registry) {
|
|
117
|
+
return registry
|
|
118
|
+
.register(sampleNoopStepper())
|
|
119
|
+
.register(projectMapStepper())
|
|
120
|
+
.register(deterministicScanStepper())
|
|
121
|
+
.register(securityTestPlanStepper())
|
|
122
|
+
.register(ralphTaskStepper())
|
|
123
|
+
.register(ralphComparisonStepper())
|
|
124
|
+
.register(ralphAcceptStepper())
|
|
125
|
+
.register(ralphFailureTrackStepper())
|
|
126
|
+
.register(sampleArtifactStepper())
|
|
127
|
+
.register(sampleNeedsHumanStepper());
|
|
128
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const RAW_SOURCE_KEYS = new Set([
|
|
2
|
+
'code',
|
|
3
|
+
'sourcecode',
|
|
4
|
+
'source_code',
|
|
5
|
+
'rawsource',
|
|
6
|
+
'raw_source',
|
|
7
|
+
'sourceblob',
|
|
8
|
+
'source_blob',
|
|
9
|
+
'filecontent',
|
|
10
|
+
'file_content',
|
|
11
|
+
'content',
|
|
12
|
+
'contents',
|
|
13
|
+
'secret',
|
|
14
|
+
'secrets',
|
|
15
|
+
'apikey',
|
|
16
|
+
'api_key',
|
|
17
|
+
'privatekey',
|
|
18
|
+
'private_key',
|
|
19
|
+
'tokenvalue',
|
|
20
|
+
'token_value',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const RAW_SOURCE_VALUE_PATTERNS = [
|
|
24
|
+
/\bsk_(?:live|test)_[a-zA-Z0-9_=-]{8,}\b/i,
|
|
25
|
+
/-----BEGIN [A-Z ]*(?:PRIVATE KEY|SECRET|TOKEN|CERTIFICATE)-----/i,
|
|
26
|
+
/\b(?:const|let|var)\s+[a-zA-Z_$][\w$]*\s*=/,
|
|
27
|
+
/\b(?:async\s+)?function\s+[a-zA-Z_$][\w$]*\s*\(/,
|
|
28
|
+
/^\s*(?:def|class)\s+[a-zA-Z_][\w]*\s*(?:\([^)]*\))?\s*:/m,
|
|
29
|
+
/=>\s*[{(]/,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const normalizeKey = (key) => String(key || '').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase();
|
|
33
|
+
|
|
34
|
+
export function findRawSourceField(value, path = 'payload', seen = new WeakSet()) {
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
return RAW_SOURCE_VALUE_PATTERNS.some((pattern) => pattern.test(value)) ? path : null;
|
|
37
|
+
}
|
|
38
|
+
if (!value || typeof value !== 'object') return null;
|
|
39
|
+
if (seen.has(value)) return null;
|
|
40
|
+
seen.add(value);
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
44
|
+
const found = findRawSourceField(value[i], `${path}[${i}]`, seen);
|
|
45
|
+
if (found) return found;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const [key, child] of Object.entries(value)) {
|
|
51
|
+
const childPath = `${path}.${key}`;
|
|
52
|
+
if (RAW_SOURCE_KEYS.has(normalizeKey(key))) return childPath;
|
|
53
|
+
const found = findRawSourceField(child, childPath, seen);
|
|
54
|
+
if (found) return found;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function assertSourceSafePayload(value, label = 'payload') {
|
|
60
|
+
const found = findRawSourceField(value, label);
|
|
61
|
+
if (found) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Deep Scan stores metadata only; raw source or secret-like value at "${found}" is not source-safe.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function cloneSourceSafe(value, label = 'payload') {
|
|
70
|
+
assertSourceSafePayload(value, label);
|
|
71
|
+
if (value === undefined) return undefined;
|
|
72
|
+
return JSON.parse(JSON.stringify(value));
|
|
73
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const STEP_ORDER = ['pending', 'running', 'needs_human', 'blocked', 'failed', 'skipped', 'passed'];
|
|
2
|
+
|
|
3
|
+
function uniqueArtifactRefs(artifacts = {}) {
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const refs = [];
|
|
6
|
+
for (const ref of Object.values(artifacts)) {
|
|
7
|
+
const key = ref?.id || `${ref?.type || 'artifact'}:${ref?.uri || ''}:${ref?.hash || ''}`;
|
|
8
|
+
if (seen.has(key)) continue;
|
|
9
|
+
seen.add(key);
|
|
10
|
+
refs.push(ref);
|
|
11
|
+
}
|
|
12
|
+
return refs;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function nextActionForState(state) {
|
|
16
|
+
if (state.status === 'pending') return 'Start or resume the local MCP Deep Scan runtime for this run.';
|
|
17
|
+
if (state.status === 'running') return 'Wait for the current stepper to checkpoint, then inspect status again.';
|
|
18
|
+
if (state.status === 'needs_human') return 'Record the required human approval or denial, then resume the run.';
|
|
19
|
+
if (state.status === 'blocked') return 'Resolve the blocked reason shown on the step result, then resume when safe.';
|
|
20
|
+
if (state.status === 'failed') return 'Inspect failed step evidence and rerun after the local issue is fixed.';
|
|
21
|
+
if (state.status === 'passed') return 'Use the receipt-backed artifacts to generate the next report or passport step.';
|
|
22
|
+
if (state.status === 'skipped') return 'Review skipped reasons before relying on the run.';
|
|
23
|
+
return 'Inspect Deep Scan status for the next safe action.';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildDeepScanStatus(state) {
|
|
27
|
+
const profileOrder = new Map((state.graphProfile?.steppers || [])
|
|
28
|
+
.map((step, index) => [step.id, index]));
|
|
29
|
+
const steps = Object.values(state.stepResults || {})
|
|
30
|
+
.map((result) => ({
|
|
31
|
+
stepperId: result.stepperId,
|
|
32
|
+
version: result.version,
|
|
33
|
+
status: result.status,
|
|
34
|
+
summary: result.summary,
|
|
35
|
+
skippedReason: result.skippedReason || null,
|
|
36
|
+
blockedReason: result.blockedReason || null,
|
|
37
|
+
needsHumanReason: result.needsHumanReason || null,
|
|
38
|
+
artifactCount: (result.artifacts || []).length,
|
|
39
|
+
findingCount: (result.findings || []).length,
|
|
40
|
+
receiptCount: (result.receipts || []).length,
|
|
41
|
+
startedAt: result.startedAt,
|
|
42
|
+
finishedAt: result.finishedAt,
|
|
43
|
+
}))
|
|
44
|
+
.sort((a, b) => {
|
|
45
|
+
const profileDelta = (profileOrder.get(a.stepperId) ?? Number.MAX_SAFE_INTEGER)
|
|
46
|
+
- (profileOrder.get(b.stepperId) ?? Number.MAX_SAFE_INTEGER);
|
|
47
|
+
if (profileDelta) return profileDelta;
|
|
48
|
+
const statusDelta = STEP_ORDER.indexOf(a.status) - STEP_ORDER.indexOf(b.status);
|
|
49
|
+
return statusDelta || a.stepperId.localeCompare(b.stepperId);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
runId: state.runId,
|
|
54
|
+
projectId: state.projectId,
|
|
55
|
+
projectHash: state.projectHash || null,
|
|
56
|
+
graphProfileId: state.graphProfileId,
|
|
57
|
+
graphProfileVersion: state.graphProfileVersion,
|
|
58
|
+
status: state.status,
|
|
59
|
+
stepCount: steps.length,
|
|
60
|
+
steps,
|
|
61
|
+
artifactRefs: uniqueArtifactRefs(state.artifacts),
|
|
62
|
+
findingCount: (state.findings || []).length,
|
|
63
|
+
receiptCount: (state.receipts || []).length,
|
|
64
|
+
receiptRefs: state.receipts || [],
|
|
65
|
+
approvals: state.approvals || [],
|
|
66
|
+
createdAt: state.createdAt,
|
|
67
|
+
updatedAt: state.updatedAt,
|
|
68
|
+
nextAction: nextActionForState(state),
|
|
69
|
+
};
|
|
70
|
+
}
|