@securityreviewai/securityreview-kit 0.1.50 → 0.1.52
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/README.md +105 -0
- package/bin/securityreview-kit.js +5 -0
- package/package.json +30 -24
- package/src/cli.js +109 -0
- package/src/commands/init.js +851 -0
- package/src/commands/status.js +99 -0
- package/src/commands/switch-project.js +207 -0
- package/src/generators/mcp/claude.js +85 -0
- package/src/generators/mcp/claude.test.js +64 -0
- package/src/generators/mcp/codex.js +70 -0
- package/src/generators/mcp/codex.test.js +43 -0
- package/src/generators/mcp/cursor.js +29 -0
- package/src/generators/mcp/cursor.test.js +50 -0
- package/src/generators/mcp/gemini.js +28 -0
- package/src/generators/mcp/vscode.js +29 -0
- package/src/generators/mcp/windsurf.js +27 -0
- package/src/generators/rules/antigravity.js +22 -0
- package/src/generators/rules/claude.js +87 -0
- package/src/generators/rules/claude.test.js +60 -0
- package/src/generators/rules/codex.js +141 -0
- package/src/generators/rules/codex.test.js +59 -0
- package/src/generators/rules/content.js +110 -0
- package/src/generators/rules/cursor.js +128 -0
- package/src/generators/rules/gemini.js +13 -0
- package/src/generators/rules/guardrails-init-profile.md +56 -0
- package/src/generators/rules/guardrails-profiler/SKILL.md +130 -0
- package/src/generators/rules/guardrails-profiler/references/signal-registry.json +514 -0
- package/src/generators/rules/guardrails-selection/references/category-threat-map.md +232 -0
- package/src/generators/rules/guardrails_rule.md +94 -0
- package/src/generators/rules/hooks.json +11 -0
- package/src/generators/rules/srai-profile.md +32 -0
- package/src/generators/rules/vscode.js +101 -0
- package/src/generators/rules/vscode.test.js +54 -0
- package/src/generators/rules/windsurf.js +13 -0
- package/src/utils/constants.js +95 -0
- package/src/utils/cursor-agent-path.js +67 -0
- package/src/utils/cursor-cli-permissions.js +28 -0
- package/src/utils/detect.js +27 -0
- package/src/utils/fs-helpers.js +82 -0
- package/src/utils/guardrails-profiler-bundle.js +84 -0
- package/src/utils/ide-cli-install.js +138 -0
- package/src/utils/profiler-agent.js +446 -0
- package/src/utils/profiler-agent.test.js +81 -0
- package/src/utils/srai.js +252 -0
- package/dist/api.js +0 -44
- package/dist/commands/guardrails.js +0 -13
- package/dist/commands/init.js +0 -88
- package/dist/commands/profile.js +0 -14
- package/dist/commands/status.js +0 -27
- package/dist/commands/sync.js +0 -6
- package/dist/config.js +0 -18
- package/dist/fs.js +0 -43
- package/dist/index.js +0 -44
- package/dist/profile.js +0 -113
- package/dist/scaffold/claude-code.js +0 -43
- package/dist/scaffold/codex.js +0 -41
- package/dist/scaffold/cursor.js +0 -45
- package/dist/scaffold/gemini.js +0 -10
- package/dist/scaffold/index.js +0 -22
- package/dist/scaffold/mcp.js +0 -15
- package/dist/scaffold/rules.js +0 -191
- package/dist/scaffold/vibreview.js +0 -30
- package/dist/scaffold/vscode.js +0 -28
- package/dist/scaffold/windsurf.js +0 -10
- package/dist/sync/index.js +0 -34
- package/dist/sync/payload.js +0 -23
- package/dist/sync/state.js +0 -12
- package/dist/types.js +0 -1
- package/templates/claude/CLAUDE.md +0 -13
- package/templates/claude/agents/guardrail_profiler.md +0 -12
- package/templates/claude/agents/threat_modeler.md +0 -5
- package/templates/claude/skills/vibreview/SKILL.md +0 -21
- package/templates/claude/skills/vibreview/guardrail_patterns.md +0 -12
- package/templates/cursor/rules/vibreview-security.mdc +0 -8
- /package/{templates/shared → src/generators/rules}/content.md +0 -0
- /package/{templates/shared/guardrails-selection.md → src/generators/rules/guardrails-selection/SKILL.md} +0 -0
- /package/{templates/shared/threat-modelling.md → src/generators/rules/skill.md} +0 -0
- /package/{templates/shared → src/generators/rules}/vibereview-sync/SKILL.md +0 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { GUARDRAILS_PROFILER_SKILL_REL_DIR, MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from './constants.js';
|
|
4
|
+
import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
|
|
5
|
+
import { readJson, readText, writeText } from './fs-helpers.js';
|
|
6
|
+
|
|
7
|
+
const PREFERRED_ORDER = ['cursor', 'vscode', 'claude', 'codex'];
|
|
8
|
+
|
|
9
|
+
function commandOk(cmd, args = ['--version'], env = process.env) {
|
|
10
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore', env });
|
|
11
|
+
return r.status === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getProfilerLogPath(cwd, target) {
|
|
15
|
+
return join(cwd, '.guardrails', 'logs', `profiler-${target}.log`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildProfilerEnv(target, streamProgress, extraEnv = {}) {
|
|
19
|
+
const env = augmentPathEnv(process.env);
|
|
20
|
+
for (const [key, value] of Object.entries(extraEnv || {})) {
|
|
21
|
+
if (value == null || value === '') {
|
|
22
|
+
delete env[key];
|
|
23
|
+
} else {
|
|
24
|
+
env[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (target === 'codex' && streamProgress && !env.RUST_LOG) {
|
|
28
|
+
env.RUST_LOG = 'info';
|
|
29
|
+
}
|
|
30
|
+
return env;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function persistProfilerLog(cwd, target, result) {
|
|
34
|
+
if (!result || (result.stdout == null && result.stderr == null)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const stdout = String(result.stdout || '');
|
|
39
|
+
const stderr = String(result.stderr || '');
|
|
40
|
+
const body = [
|
|
41
|
+
`target=${target}`,
|
|
42
|
+
`status=${result.status ?? ''}`,
|
|
43
|
+
`signal=${result.signal ?? ''}`,
|
|
44
|
+
'',
|
|
45
|
+
'--- stdout ---',
|
|
46
|
+
stdout,
|
|
47
|
+
'',
|
|
48
|
+
'--- stderr ---',
|
|
49
|
+
stderr,
|
|
50
|
+
'',
|
|
51
|
+
].join('\n');
|
|
52
|
+
|
|
53
|
+
const logPath = getProfilerLogPath(cwd, target);
|
|
54
|
+
writeText(logPath, body);
|
|
55
|
+
return logPath;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildProfilerAgentPrompt(projectName, agentTarget = 'cursor') {
|
|
59
|
+
const p = String(projectName || '').trim() || '<SRAI_PROJECT_NAME>';
|
|
60
|
+
const skillRoot =
|
|
61
|
+
GUARDRAILS_PROFILER_SKILL_REL_DIR[agentTarget] ||
|
|
62
|
+
GUARDRAILS_PROFILER_SKILL_REL_DIR.cursor;
|
|
63
|
+
return [
|
|
64
|
+
'Security Review Kit: run guardrails initialization profiling for this workspace.',
|
|
65
|
+
`SRAI project name: "${p}".`,
|
|
66
|
+
`Open and follow every step in ${skillRoot}/SKILL.md.`,
|
|
67
|
+
`Use ${skillRoot}/references/signal-registry.json as the signal registry.`,
|
|
68
|
+
'Write .guardrails/profile.json as defined in the skill.',
|
|
69
|
+
`Resolve project_id with find_project_by_name for "${p}", then call update_vibe_profile and write_default_pack via security-review-mcp.`,
|
|
70
|
+
'Do not invent stack details, compliance, or user groups; ground everything in the repository.',
|
|
71
|
+
].join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run Cursor Agent OAuth/login in the current terminal (stdio inherited).
|
|
76
|
+
* Call this from init before profiling so the user does not leave the kit flow.
|
|
77
|
+
*/
|
|
78
|
+
export function runCursorAgentLogin(cwd) {
|
|
79
|
+
const bin = resolveCursorAgentExecutable();
|
|
80
|
+
if (!bin) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
status: null,
|
|
84
|
+
message:
|
|
85
|
+
'Cursor Agent CLI not found (`agent` or `cursor-agent`). Install from https://cursor.com/docs/cli/installation (add ~/.local/bin to PATH), or re-run init without --skip-ide-cli-install.',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const env = augmentPathEnv(process.env);
|
|
89
|
+
const r = spawnSync(bin, ['login'], { cwd, stdio: 'inherit', env });
|
|
90
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
91
|
+
if (r.status === null && spawnErr) {
|
|
92
|
+
return { ok: false, status: null, message: spawnErr };
|
|
93
|
+
}
|
|
94
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run GitHub Copilot CLI OAuth login in the current terminal (stdio inherited).
|
|
99
|
+
* `copilot login` exits after the device-flow succeeds, so init can continue into profiling.
|
|
100
|
+
*/
|
|
101
|
+
export function runCopilotLogin(cwd) {
|
|
102
|
+
const env = augmentPathEnv(process.env);
|
|
103
|
+
if (!commandOk('copilot', ['--version'], env)) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
status: null,
|
|
107
|
+
message:
|
|
108
|
+
'GitHub Copilot CLI not found (`copilot`). Install from https://docs.github.com/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli.',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const r = spawnSync('copilot', ['login'], { cwd, stdio: 'inherit', env });
|
|
112
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
113
|
+
if (r.status === null && spawnErr) {
|
|
114
|
+
return { ok: false, status: null, message: spawnErr };
|
|
115
|
+
}
|
|
116
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Run Claude Code login in the current terminal.
|
|
121
|
+
*/
|
|
122
|
+
export function runClaudeLogin(cwd) {
|
|
123
|
+
return runClaudeAuthLogin(cwd, { mode: 'claudeai' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run Claude Code auth login in the current terminal.
|
|
128
|
+
*/
|
|
129
|
+
export function runClaudeAuthLogin(cwd, options = {}) {
|
|
130
|
+
const env = augmentPathEnv(process.env);
|
|
131
|
+
if (!commandOk('claude', ['--version'], env)) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
status: null,
|
|
135
|
+
message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const mode = options.mode === 'console' ? '--console' : '--claudeai';
|
|
139
|
+
const r = spawnSync('claude', ['auth', 'login', mode], { cwd, stdio: 'inherit', env });
|
|
140
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
141
|
+
if (r.status === null && spawnErr) {
|
|
142
|
+
return { ok: false, status: null, message: spawnErr };
|
|
143
|
+
}
|
|
144
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Run Claude Code long-lived token setup in the current terminal.
|
|
149
|
+
*/
|
|
150
|
+
export function runClaudeSetupToken(cwd) {
|
|
151
|
+
const env = augmentPathEnv(process.env);
|
|
152
|
+
if (!commandOk('claude', ['--version'], env)) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
status: null,
|
|
156
|
+
message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const r = spawnSync('claude', ['setup-token'], { cwd, stdio: 'inherit', env });
|
|
160
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
161
|
+
if (r.status === null && spawnErr) {
|
|
162
|
+
return { ok: false, status: null, message: spawnErr };
|
|
163
|
+
}
|
|
164
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run Codex login in the current terminal. Device auth keeps the flow in-terminal.
|
|
169
|
+
*/
|
|
170
|
+
export function runCodexLogin(cwd) {
|
|
171
|
+
const env = augmentPathEnv(process.env);
|
|
172
|
+
if (!commandOk('codex', ['--version'], env)) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
status: null,
|
|
176
|
+
message: 'Codex CLI not found (`codex`). Install with `npm install -g @openai/codex`.',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const r = spawnSync('codex', ['login', '--device-auth'], { cwd, stdio: 'inherit', env });
|
|
180
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
181
|
+
if (r.status === null && spawnErr) {
|
|
182
|
+
return { ok: false, status: null, message: spawnErr };
|
|
183
|
+
}
|
|
184
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function escapeTomlString(value) {
|
|
188
|
+
return String(value || '').replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readCodexMcpEnv(cwd) {
|
|
192
|
+
const config = readText(join(cwd, '.codex', 'config.toml'));
|
|
193
|
+
const apiUrl = config.match(/^\s*SECURITY_REVIEW_API_URL\s*=\s*"([^"]*)"/m)?.[1] || '';
|
|
194
|
+
const apiToken = config.match(/^\s*SECURITY_REVIEW_API_TOKEN\s*=\s*"([^"]*)"/m)?.[1] || '';
|
|
195
|
+
return { apiUrl, apiToken };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function buildCodexConfigOverrides(cwd, overrides = {}) {
|
|
199
|
+
const fileValues = readCodexMcpEnv(cwd);
|
|
200
|
+
const apiUrl = String(overrides.apiUrl || fileValues.apiUrl || '').trim();
|
|
201
|
+
const apiToken = String(overrides.apiToken || fileValues.apiToken || '').trim();
|
|
202
|
+
|
|
203
|
+
if (!apiUrl || !apiToken) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return [
|
|
208
|
+
'features.codex_hooks=true',
|
|
209
|
+
`mcp_servers.${MCP_SERVER_NAME}.command="npx"`,
|
|
210
|
+
`mcp_servers.${MCP_SERVER_NAME}.args=["-y","${MCP_SERVER_PACKAGE}@latest"]`,
|
|
211
|
+
`mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_URL="${escapeTomlString(apiUrl)}"`,
|
|
212
|
+
`mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_TOKEN="${escapeTomlString(apiToken)}"`,
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function buildClaudeAdditionalMcpConfig(cwd) {
|
|
217
|
+
const projectMcp = readJson(join(cwd, '.mcp.json'));
|
|
218
|
+
const sourceServer = projectMcp?.mcpServers?.[MCP_SERVER_NAME];
|
|
219
|
+
|
|
220
|
+
if (!sourceServer || typeof sourceServer !== 'object') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return JSON.stringify({
|
|
225
|
+
mcpServers: {
|
|
226
|
+
[MCP_SERVER_NAME]: sourceServer,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function pickProfilerAgentTarget(targets) {
|
|
232
|
+
for (const t of PREFERRED_ORDER) {
|
|
233
|
+
if (targets.includes(t)) {
|
|
234
|
+
return t;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Spawn the IDE agent CLI to execute the profiler skill (user must be logged in where required).
|
|
242
|
+
*
|
|
243
|
+
* @param {object} opts
|
|
244
|
+
* @param {boolean} [opts.cursorTrust=true] When true, passes `--trust` and `--approve-mcps` so headless init is not blocked by
|
|
245
|
+
* workspace trust or MCP approval (user confirmed profiling in the kit). Set false with `--profiler-no-trust`
|
|
246
|
+
* if you need an interactive trust/login/MCP flow in the same terminal.
|
|
247
|
+
* @param {boolean} [opts.streamProgress=false] When true, pass each CLI’s streaming / verbose flags.
|
|
248
|
+
* @param {boolean} [opts.showOutput=false] When true, inherit stdio from the child process.
|
|
249
|
+
* Keep both false for init-time profiling so the agent does not flood the terminal with JSON/progress logs.
|
|
250
|
+
*/
|
|
251
|
+
export function runProfilerAgent(
|
|
252
|
+
cwd,
|
|
253
|
+
{
|
|
254
|
+
target,
|
|
255
|
+
projectName,
|
|
256
|
+
apiUrl,
|
|
257
|
+
apiToken,
|
|
258
|
+
modelOverride,
|
|
259
|
+
extraEnv,
|
|
260
|
+
cursorTrust = true,
|
|
261
|
+
streamProgress = false,
|
|
262
|
+
showOutput = streamProgress,
|
|
263
|
+
},
|
|
264
|
+
) {
|
|
265
|
+
const prompt = buildProfilerAgentPrompt(projectName, target);
|
|
266
|
+
const env = buildProfilerEnv(target, streamProgress, extraEnv);
|
|
267
|
+
const opts = showOutput
|
|
268
|
+
? { cwd, stdio: 'inherit', env }
|
|
269
|
+
: { cwd, stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 };
|
|
270
|
+
|
|
271
|
+
if (streamProgress) {
|
|
272
|
+
console.error(
|
|
273
|
+
'\n[securityreview-kit] Profiler live output: you should see streaming progress below ' +
|
|
274
|
+
'(JSON lines are normal). Omit --profiler-verbose for minimal output.\n',
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (target === 'cursor') {
|
|
279
|
+
const bin = resolveCursorAgentExecutable();
|
|
280
|
+
if (!bin) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
message:
|
|
284
|
+
'Cursor Agent CLI not found (`agent` or `cursor-agent`). Install from https://cursor.com/docs/cli/installation and ensure ~/.local/bin exists; run init Step 1b without --skip-ide-cli-install (Node subprocesses get an augmented PATH, but the binary must be installed).',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const args = ['-p', prompt];
|
|
288
|
+
if (streamProgress) {
|
|
289
|
+
args.push('--output-format', 'stream-json', '--stream-partial-output');
|
|
290
|
+
}
|
|
291
|
+
if (cursorTrust) {
|
|
292
|
+
args.push('--trust', '--approve-mcps');
|
|
293
|
+
}
|
|
294
|
+
const r = spawnSync(bin, args, opts);
|
|
295
|
+
if (r.error) {
|
|
296
|
+
return { ok: false, message: r.error.message };
|
|
297
|
+
}
|
|
298
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (target === 'claude') {
|
|
302
|
+
if (!commandOk('claude', ['--version'], env)) {
|
|
303
|
+
return { ok: false, message: 'claude not on PATH' };
|
|
304
|
+
}
|
|
305
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
306
|
+
const mcpConfig = buildClaudeAdditionalMcpConfig(cwd);
|
|
307
|
+
if (!mcpConfig) {
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
message:
|
|
311
|
+
'Claude profiling needs the SRAI MCP server in .mcp.json. Re-run init with MCP installation enabled.',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const args = [
|
|
315
|
+
'-p',
|
|
316
|
+
'--settings',
|
|
317
|
+
settingsPath,
|
|
318
|
+
'--mcp-config',
|
|
319
|
+
mcpConfig,
|
|
320
|
+
'--strict-mcp-config',
|
|
321
|
+
'--permission-mode',
|
|
322
|
+
'bypassPermissions',
|
|
323
|
+
'--model',
|
|
324
|
+
modelOverride || 'haiku',
|
|
325
|
+
];
|
|
326
|
+
if (streamProgress) {
|
|
327
|
+
args.push('--output-format', 'stream-json', '--include-partial-messages', '--include-hook-events', '--verbose');
|
|
328
|
+
}
|
|
329
|
+
args.push(prompt);
|
|
330
|
+
const r = spawnSync('claude', args, opts);
|
|
331
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (target === 'codex') {
|
|
335
|
+
if (!commandOk('codex', ['--version'], env)) {
|
|
336
|
+
return { ok: false, message: 'codex not on PATH' };
|
|
337
|
+
}
|
|
338
|
+
const configOverrides = buildCodexConfigOverrides(cwd, { apiUrl, apiToken });
|
|
339
|
+
if (!configOverrides) {
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
message:
|
|
343
|
+
'Codex profiling needs SRAI MCP credentials. Re-run init with Codex selected and MCP installation enabled.',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const args = ['exec', '--full-auto'];
|
|
347
|
+
for (const override of configOverrides) {
|
|
348
|
+
args.push('-c', override);
|
|
349
|
+
}
|
|
350
|
+
if (streamProgress) {
|
|
351
|
+
args.push('--json');
|
|
352
|
+
}
|
|
353
|
+
args.push(prompt);
|
|
354
|
+
const r = spawnSync('codex', args, opts);
|
|
355
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (target === 'vscode') {
|
|
359
|
+
if (!commandOk('copilot', ['--version'], env)) {
|
|
360
|
+
return { ok: false, message: 'copilot not on PATH' };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const mcpConfig = buildCopilotAdditionalMcpConfig(cwd);
|
|
364
|
+
if (!mcpConfig) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
message:
|
|
368
|
+
'VS Code MCP config not found for security-review-mcp. Re-run init with MCP installation enabled.',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const args = [
|
|
373
|
+
'-p',
|
|
374
|
+
prompt,
|
|
375
|
+
'--additional-mcp-config',
|
|
376
|
+
mcpConfig,
|
|
377
|
+
'--allow-all',
|
|
378
|
+
];
|
|
379
|
+
if (streamProgress) {
|
|
380
|
+
args.push('--output-format', 'json');
|
|
381
|
+
}
|
|
382
|
+
const r = spawnSync('copilot', args, opts);
|
|
383
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { ok: false, message: 'unsupported agent target' };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function buildCopilotAdditionalMcpConfig(cwd) {
|
|
390
|
+
const vscodeMcp = readJson(join(cwd, '.vscode', 'mcp.json'));
|
|
391
|
+
const sourceServer =
|
|
392
|
+
vscodeMcp?.mcpServers?.[MCP_SERVER_NAME] || vscodeMcp?.servers?.[MCP_SERVER_NAME];
|
|
393
|
+
|
|
394
|
+
if (!sourceServer || typeof sourceServer !== 'object') {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const server = { ...sourceServer };
|
|
399
|
+
if (!server.type) {
|
|
400
|
+
server.type = 'stdio';
|
|
401
|
+
}
|
|
402
|
+
if (!Array.isArray(server.tools)) {
|
|
403
|
+
server.tools = ['*'];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return JSON.stringify({
|
|
407
|
+
mcpServers: {
|
|
408
|
+
[MCP_SERVER_NAME]: server,
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function buildProfilerResult(result, extras = {}) {
|
|
414
|
+
if (result.error) {
|
|
415
|
+
return { ok: false, status: result.status, message: result.error.message, ...extras };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (result.status === 0) {
|
|
419
|
+
return { ok: true, status: result.status, ...extras };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const outputTail = summarizeProfilerOutput(result.stderr || result.stdout || '');
|
|
423
|
+
const exitDetail =
|
|
424
|
+
typeof result.status === 'number'
|
|
425
|
+
? `exit status ${result.status}`
|
|
426
|
+
: result.signal
|
|
427
|
+
? `signal ${result.signal}`
|
|
428
|
+
: 'unknown error';
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
status: result.status,
|
|
433
|
+
message: outputTail ? `${exitDetail}; last output: ${outputTail}` : exitDetail,
|
|
434
|
+
...extras,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function summarizeProfilerOutput(output) {
|
|
439
|
+
return String(output || '')
|
|
440
|
+
.replace(/\u001b\[[0-9;]*m/g, '')
|
|
441
|
+
.split(/\r?\n/)
|
|
442
|
+
.map((line) => line.trim())
|
|
443
|
+
.filter(Boolean)
|
|
444
|
+
.slice(-3)
|
|
445
|
+
.join(' | ');
|
|
446
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import {
|
|
7
|
+
buildCodexConfigOverrides,
|
|
8
|
+
buildCopilotAdditionalMcpConfig,
|
|
9
|
+
buildProfilerAgentPrompt,
|
|
10
|
+
pickProfilerAgentTarget,
|
|
11
|
+
} from './profiler-agent.js';
|
|
12
|
+
|
|
13
|
+
test('pickProfilerAgentTarget supports VS Code Copilot', () => {
|
|
14
|
+
assert.equal(pickProfilerAgentTarget(['vscode']), 'vscode');
|
|
15
|
+
assert.equal(pickProfilerAgentTarget(['codex', 'vscode']), 'vscode');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('buildCopilotAdditionalMcpConfig converts VS Code MCP config', () => {
|
|
19
|
+
const cwd = join(tmpdir(), `securityreview-kit-${Date.now()}`);
|
|
20
|
+
mkdirSync(join(cwd, '.vscode'), { recursive: true });
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(cwd, '.vscode', 'mcp.json'),
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
servers: {
|
|
25
|
+
'security-review-mcp': {
|
|
26
|
+
type: 'stdio',
|
|
27
|
+
command: 'npx',
|
|
28
|
+
args: ['-y', '@securityreviewai/security-review-mcp@latest'],
|
|
29
|
+
env: {
|
|
30
|
+
SECURITY_REVIEW_API_URL: 'https://api.example.test',
|
|
31
|
+
SECURITY_REVIEW_API_TOKEN: 'token',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const converted = JSON.parse(buildCopilotAdditionalMcpConfig(cwd));
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(Object.keys(converted.mcpServers), ['security-review-mcp']);
|
|
41
|
+
assert.equal(converted.mcpServers['security-review-mcp'].command, 'npx');
|
|
42
|
+
assert.deepEqual(converted.mcpServers['security-review-mcp'].tools, ['*']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('buildCodexConfigOverrides builds inline MCP config for profiling', () => {
|
|
46
|
+
const cwd = join(tmpdir(), `securityreview-kit-codex-${Date.now()}`);
|
|
47
|
+
mkdirSync(join(cwd, '.codex'), { recursive: true });
|
|
48
|
+
writeFileSync(
|
|
49
|
+
join(cwd, '.codex', 'config.toml'),
|
|
50
|
+
[
|
|
51
|
+
'[features]',
|
|
52
|
+
'codex_hooks = true',
|
|
53
|
+
'',
|
|
54
|
+
'[mcp_servers.security-review-mcp]',
|
|
55
|
+
'command = "npx"',
|
|
56
|
+
'args = ["-y", "@securityreviewai/security-review-mcp@latest"]',
|
|
57
|
+
'',
|
|
58
|
+
'[mcp_servers.security-review-mcp.env]',
|
|
59
|
+
'SECURITY_REVIEW_API_URL = "https://api.example.test"',
|
|
60
|
+
'SECURITY_REVIEW_API_TOKEN = "token"',
|
|
61
|
+
'',
|
|
62
|
+
].join('\n'),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const overrides = buildCodexConfigOverrides(cwd);
|
|
66
|
+
|
|
67
|
+
assert.deepEqual(overrides, [
|
|
68
|
+
'features.codex_hooks=true',
|
|
69
|
+
'mcp_servers.security-review-mcp.command="npx"',
|
|
70
|
+
'mcp_servers.security-review-mcp.args=["-y","@securityreviewai/security-review-mcp@latest"]',
|
|
71
|
+
'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_URL="https://api.example.test"',
|
|
72
|
+
'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_TOKEN="token"',
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('buildProfilerAgentPrompt only instructs writing the guardrails profile under .guardrails', () => {
|
|
77
|
+
const prompt = buildProfilerAgentPrompt('demo-project', 'codex');
|
|
78
|
+
|
|
79
|
+
assert.match(prompt, /Write \.guardrails\/profile\.json as defined in the skill\./);
|
|
80
|
+
assert.doesNotMatch(prompt, /profile\.json at the repository root/);
|
|
81
|
+
});
|