@securityreviewai/securityreview-kit 0.1.37 → 0.1.39
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 +6 -2
- package/package.json +1 -1
- package/src/cli.js +12 -0
- package/src/commands/init.js +112 -3
- package/src/generators/mcp/claude.js +35 -0
- package/src/generators/mcp/claude.test.js +53 -0
- package/src/generators/rules/claude.js +66 -11
- package/src/generators/rules/claude.test.js +39 -0
- package/src/generators/rules/codex.js +23 -5
- package/src/generators/rules/codex.test.js +9 -6
- package/src/generators/rules/guardrails-init-profile.md +14 -0
- package/src/generators/rules/guardrails-profiler/SKILL.md +1 -1
- package/src/utils/constants.js +6 -4
- package/src/utils/profiler-agent.js +148 -15
- package/src/utils/profiler-agent.test.js +32 -0
package/README.md
CHANGED
|
@@ -28,10 +28,10 @@ npx @securityreviewai/securityreview-kit init --switch-project
|
|
|
28
28
|
| Target | Flag | MCP Config | Workspace Rule |
|
|
29
29
|
|---|---|---|---|
|
|
30
30
|
| Cursor | `cursor` | `.cursor/mcp.json` | `.cursor/rules/srai-security-review.mdc`, `.cursor/rules/ctm_sync_rule.mdc`, `.cursor/commands/ctm_sync.md`, `.cursor/agents/ctm_sync.md`, `.cursor/commands/create-ide-workflow.md`, `.cursor/commands/srai-profile.md`, `.cursor/skills/threat-modelling/SKILL.md` |
|
|
31
|
-
| Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md` |
|
|
31
|
+
| Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md`, `.claude/skills/threat-modelling/SKILL.md`, `.claude/skills/guardrails-profiler/SKILL.md`, `.claude/skills/guardrails-selection/SKILL.md`, `.claude/agents/ctm_sync.md`, `.claude/commands/guardrails-init-profile.md` |
|
|
32
32
|
| VS Code Copilot | `vscode` | `.vscode/mcp.json` | `.github/copilot-instructions.md`, `.github/skills/threat-modelling/SKILL.md`, `.github/skills/guardrails-profiler/SKILL.md`, `.github/skills/guardrails-selection/SKILL.md`, `.github/agents/ctm_sync.agent.md`, `.github/hooks/srai-session-policy.json` |
|
|
33
33
|
| Windsurf | `windsurf` | `.windsurf/mcp_config.json` | `.windsurf/rules/srai-security-review.md` |
|
|
34
|
-
| Codex | `codex` | `.codex/config.toml` |
|
|
34
|
+
| Codex | `codex` | `.codex/config.toml` | `.codex/AGENTS.md`, `.codex/skills/threat-modelling/SKILL.md`, `.codex/skills/guardrails-profiler/SKILL.md`, `.codex/skills/guardrails-selection/SKILL.md`, `.codex/agents/ctm_sync.toml`, `.codex/hooks.json`, `.codex/commands/guardrails-init-profile.md` |
|
|
35
35
|
| Gemini CLI | `gemini` | `.gemini/settings.json` | `GEMINI.md` |
|
|
36
36
|
| Antigravity | `antigravity` | `.gemini/settings.json` | `.agents/rules/srai-security-review.md` |
|
|
37
37
|
|
|
@@ -52,8 +52,12 @@ Options:
|
|
|
52
52
|
--skip-mcp Skip MCP server config installation
|
|
53
53
|
--skip-rules Skip workspace rule installation
|
|
54
54
|
--profile-repo Run the guardrails profiler after init
|
|
55
|
+
--profiler-claude-login Run Claude Code login before profiling
|
|
55
56
|
--profiler-copilot-login
|
|
56
57
|
Run GitHub Copilot CLI login before VS Code Copilot profiling
|
|
58
|
+
--profiler-codex-login Run Codex login before Codex profiling
|
|
59
|
+
--profiler-verbose Show live profiler output while profiling runs
|
|
60
|
+
--show-profiler-logs Alias for --profiler-verbose
|
|
57
61
|
```
|
|
58
62
|
|
|
59
63
|
### `@securityreviewai/securityreview-kit init --switch-project`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -37,10 +37,18 @@ export function run() {
|
|
|
37
37
|
'--profiler-cursor-login',
|
|
38
38
|
'Before Cursor profiling, run `agent login` (or `cursor-agent login`) in this terminal (then profiling runs in the same init)',
|
|
39
39
|
)
|
|
40
|
+
.option(
|
|
41
|
+
'--profiler-claude-login',
|
|
42
|
+
'Before Claude Code profiling, run `claude auth login` in this terminal',
|
|
43
|
+
)
|
|
40
44
|
.option(
|
|
41
45
|
'--profiler-copilot-login',
|
|
42
46
|
'Before VS Code Copilot profiling, run `copilot login` in this terminal',
|
|
43
47
|
)
|
|
48
|
+
.option(
|
|
49
|
+
'--profiler-codex-login',
|
|
50
|
+
'Before Codex profiling, run `codex login --device-auth` in this terminal',
|
|
51
|
+
)
|
|
44
52
|
.option(
|
|
45
53
|
'--profiler-quiet',
|
|
46
54
|
'When profiling, use the standard progress message (default; retained for compatibility)',
|
|
@@ -49,6 +57,10 @@ export function run() {
|
|
|
49
57
|
'--profiler-verbose',
|
|
50
58
|
'When profiling, show live agent output for troubleshooting',
|
|
51
59
|
)
|
|
60
|
+
.option(
|
|
61
|
+
'--show-profiler-logs',
|
|
62
|
+
'Alias for --profiler-verbose; show live profiler logs while profiling runs',
|
|
63
|
+
)
|
|
52
64
|
.action(async (options) => {
|
|
53
65
|
try {
|
|
54
66
|
if (options.switchProject) {
|
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,8 @@ import { ensureIdeClisForTargets } from '../utils/ide-cli-install.js';
|
|
|
6
6
|
import { writeGuardrailsSkillBundles } from '../utils/guardrails-profiler-bundle.js';
|
|
7
7
|
import {
|
|
8
8
|
pickProfilerAgentTarget,
|
|
9
|
+
runClaudeLogin,
|
|
10
|
+
runCodexLogin,
|
|
9
11
|
runCopilotLogin,
|
|
10
12
|
runCursorAgentLogin,
|
|
11
13
|
runProfilerAgent,
|
|
@@ -485,27 +487,95 @@ export async function initCommand(options) {
|
|
|
485
487
|
}
|
|
486
488
|
console.log('');
|
|
487
489
|
}
|
|
490
|
+
} else if (agentTarget === 'claude') {
|
|
491
|
+
console.log(
|
|
492
|
+
chalk.dim(
|
|
493
|
+
' Claude Code: profiling uses `.claude/settings.json`, the configured MCP server, and the Haiku model for this run.',
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
let runLogin = Boolean(options.profilerClaudeLogin);
|
|
498
|
+
if (!runLogin && interactive) {
|
|
499
|
+
runLogin = await confirm({
|
|
500
|
+
message:
|
|
501
|
+
'Run Claude Code login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
|
|
502
|
+
default: true,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (runLogin) {
|
|
506
|
+
console.log('');
|
|
507
|
+
console.log(chalk.bold.white(' Claude Code login'));
|
|
508
|
+
console.log(chalk.dim(' Complete the browser prompt, then return here.\n'));
|
|
509
|
+
const loginResult = runClaudeLogin(cwd);
|
|
510
|
+
if (loginResult.ok) {
|
|
511
|
+
console.log(chalk.green(' \u2713 Claude Code login step finished.'));
|
|
512
|
+
} else {
|
|
513
|
+
console.log(
|
|
514
|
+
chalk.yellow(
|
|
515
|
+
` \u26a0 Claude login exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; sign in and re-run init if it fails.`,
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
console.log('');
|
|
520
|
+
}
|
|
521
|
+
} else if (agentTarget === 'codex') {
|
|
522
|
+
console.log(
|
|
523
|
+
chalk.dim(
|
|
524
|
+
' Codex: profiling passes the SRAI MCP server directly to `codex exec` for this run.',
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
let runLogin = Boolean(options.profilerCodexLogin);
|
|
529
|
+
if (!runLogin && interactive) {
|
|
530
|
+
runLogin = await confirm({
|
|
531
|
+
message:
|
|
532
|
+
'Run Codex login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
|
|
533
|
+
default: true,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (runLogin) {
|
|
537
|
+
console.log('');
|
|
538
|
+
console.log(chalk.bold.white(' Codex login'));
|
|
539
|
+
console.log(chalk.dim(' Complete the device-code prompt, then return here.\n'));
|
|
540
|
+
const loginResult = runCodexLogin(cwd);
|
|
541
|
+
if (loginResult.ok) {
|
|
542
|
+
console.log(chalk.green(' \u2713 Codex login step finished.'));
|
|
543
|
+
} else {
|
|
544
|
+
console.log(
|
|
545
|
+
chalk.yellow(
|
|
546
|
+
` \u26a0 Codex login exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; sign in and re-run init if it fails.`,
|
|
547
|
+
),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
console.log('');
|
|
551
|
+
}
|
|
488
552
|
} else {
|
|
489
553
|
console.log(chalk.dim(' (Sign-in or approvals may be required in your terminal.)'));
|
|
490
554
|
}
|
|
491
555
|
console.log('');
|
|
556
|
+
const profilerVerbose = Boolean(options.profilerVerbose || options.showProfilerLogs);
|
|
492
557
|
const showProfilerOutput = Boolean(
|
|
493
|
-
|
|
558
|
+
profilerVerbose || (agentTarget === 'cursor' && options.profilerNoTrust),
|
|
494
559
|
);
|
|
495
560
|
if (showProfilerOutput) {
|
|
496
|
-
console.log(chalk.dim(' Profiling in progress.
|
|
561
|
+
console.log(chalk.dim(' Profiling in progress. Live profiler logs are visible for this run...'));
|
|
497
562
|
} else {
|
|
498
563
|
console.log(chalk.dim(' Profiling in progress. This can take a few minutes...'));
|
|
499
564
|
}
|
|
500
565
|
const pr = runProfilerAgent(cwd, {
|
|
501
566
|
target: agentTarget,
|
|
502
567
|
projectName: projectNameForSkill,
|
|
568
|
+
apiUrl: envVars?.apiUrl,
|
|
569
|
+
apiToken: envVars?.apiToken,
|
|
503
570
|
cursorTrust: !options.profilerNoTrust,
|
|
504
|
-
streamProgress:
|
|
571
|
+
streamProgress: profilerVerbose,
|
|
505
572
|
showOutput: showProfilerOutput,
|
|
506
573
|
});
|
|
507
574
|
if (pr.ok) {
|
|
508
575
|
console.log(chalk.green(' \u2713 Profiler agent finished.'));
|
|
576
|
+
if (pr.logPath) {
|
|
577
|
+
console.log(chalk.dim(` Profiler log saved to ${pr.logPath}`));
|
|
578
|
+
}
|
|
509
579
|
} else {
|
|
510
580
|
const detail =
|
|
511
581
|
pr.message ||
|
|
@@ -515,6 +585,9 @@ export async function initCommand(options) {
|
|
|
515
585
|
` \u26a0 Profiler agent exited with an error: ${detail}. You can run the guardrails-init-profile workflow manually.`,
|
|
516
586
|
),
|
|
517
587
|
);
|
|
588
|
+
if (pr.logPath) {
|
|
589
|
+
console.log(chalk.dim(` Profiler log saved to ${pr.logPath}`));
|
|
590
|
+
}
|
|
518
591
|
if (agentTarget === 'cursor') {
|
|
519
592
|
console.log('');
|
|
520
593
|
console.log(chalk.dim(' Typical fixes:'));
|
|
@@ -551,6 +624,42 @@ export async function initCommand(options) {
|
|
|
551
624
|
' • MCP missing: re-run init with VS Code selected and MCP installation enabled so `.vscode/mcp.json` is written.',
|
|
552
625
|
),
|
|
553
626
|
);
|
|
627
|
+
} else if (agentTarget === 'claude') {
|
|
628
|
+
console.log('');
|
|
629
|
+
console.log(chalk.dim(' Typical fixes:'));
|
|
630
|
+
console.log(
|
|
631
|
+
chalk.dim(
|
|
632
|
+
' • Not signed in: re-run `securityreview-kit init` and choose Yes for Claude Code login, or pass `--profiler-claude-login` with `--profile-repo`.',
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
console.log(
|
|
636
|
+
chalk.dim(
|
|
637
|
+
' • CLI missing: install Claude Code and verify `claude --version`.',
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
console.log(
|
|
641
|
+
chalk.dim(
|
|
642
|
+
' • MCP missing: re-run init with Claude Code selected and MCP installation enabled so `.claude/settings.json` is written.',
|
|
643
|
+
),
|
|
644
|
+
);
|
|
645
|
+
} else if (agentTarget === 'codex') {
|
|
646
|
+
console.log('');
|
|
647
|
+
console.log(chalk.dim(' Typical fixes:'));
|
|
648
|
+
console.log(
|
|
649
|
+
chalk.dim(
|
|
650
|
+
' • Not signed in: re-run `securityreview-kit init` and choose Yes for Codex login, or pass `--profiler-codex-login` with `--profile-repo`.',
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
console.log(
|
|
654
|
+
chalk.dim(
|
|
655
|
+
' • CLI missing: install Codex CLI and verify `codex --version`.',
|
|
656
|
+
),
|
|
657
|
+
);
|
|
658
|
+
console.log(
|
|
659
|
+
chalk.dim(
|
|
660
|
+
' • MCP missing: re-run init with Codex selected and MCP installation enabled so the SRAI credentials are available for profiling.',
|
|
661
|
+
),
|
|
662
|
+
);
|
|
554
663
|
}
|
|
555
664
|
}
|
|
556
665
|
}
|
|
@@ -2,6 +2,32 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
3
|
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
4
|
|
|
5
|
+
function getClaudeSessionStartHooks() {
|
|
6
|
+
const prompt = [
|
|
7
|
+
'MANDATORY SECURITY GATE (Claude Code Session Policy)',
|
|
8
|
+
'',
|
|
9
|
+
'For any request with security impact, follow this order:',
|
|
10
|
+
'1. Fetch Vibe Guardrails first using .claude/skills/guardrails-selection/SKILL.md.',
|
|
11
|
+
'2. Run PWNISMS threat modelling using .claude/skills/threat-modelling/SKILL.md.',
|
|
12
|
+
'3. Implement secure code using the hydrated guardrails and threat findings.',
|
|
13
|
+
'4. Invoke the ctm_sync agent using .claude/agents/ctm_sync.md after implementation or threat-model updates.',
|
|
14
|
+
'',
|
|
15
|
+
'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, ctm_sync last.',
|
|
16
|
+
].join('\n');
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
matcher: '.*',
|
|
21
|
+
hooks: [
|
|
22
|
+
{
|
|
23
|
+
type: 'prompt',
|
|
24
|
+
prompt,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
/**
|
|
6
32
|
* Generate Claude Code MCP config at .claude/settings.json
|
|
7
33
|
*/
|
|
@@ -22,6 +48,15 @@ export function generate(cwd, envVars) {
|
|
|
22
48
|
},
|
|
23
49
|
};
|
|
24
50
|
|
|
51
|
+
const existingSessionStart = Array.isArray(existing.SessionStart) ? existing.SessionStart : [];
|
|
52
|
+
const marker = 'MANDATORY SECURITY GATE (Claude Code Session Policy)';
|
|
53
|
+
const ours = getClaudeSessionStartHooks();
|
|
54
|
+
const hasOurs = existingSessionStart.some((entry) =>
|
|
55
|
+
Array.isArray(entry?.hooks) &&
|
|
56
|
+
entry.hooks.some((hook) => hook?.type === 'prompt' && typeof hook?.prompt === 'string' && hook.prompt.includes(marker)),
|
|
57
|
+
);
|
|
58
|
+
existing.SessionStart = hasOurs ? existingSessionStart : [...existingSessionStart, ...ours];
|
|
59
|
+
|
|
25
60
|
writeJson(filePath, existing);
|
|
26
61
|
return filePath;
|
|
27
62
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, 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 { generate } from './claude.js';
|
|
7
|
+
|
|
8
|
+
test('Claude MCP generator writes mcp server and session hooks', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-'));
|
|
10
|
+
|
|
11
|
+
generate(cwd, {
|
|
12
|
+
apiUrl: 'https://example.test',
|
|
13
|
+
apiToken: 'secret-token',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const config = JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8'));
|
|
17
|
+
assert.equal(config.mcpServers['security-review-mcp'].command, 'npx');
|
|
18
|
+
assert.equal(Array.isArray(config.SessionStart), true);
|
|
19
|
+
assert.match(config.SessionStart[0].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('Claude MCP generator preserves existing SessionStart hooks', () => {
|
|
23
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-merge-'));
|
|
24
|
+
const configPath = join(cwd, '.claude', 'settings.json');
|
|
25
|
+
mkdirSync(join(cwd, '.claude'), { recursive: true });
|
|
26
|
+
|
|
27
|
+
writeFileSync(
|
|
28
|
+
configPath,
|
|
29
|
+
JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
SessionStart: [
|
|
32
|
+
{
|
|
33
|
+
matcher: '.*',
|
|
34
|
+
hooks: [{ type: 'prompt', prompt: 'Existing hook' }],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
null,
|
|
39
|
+
2,
|
|
40
|
+
),
|
|
41
|
+
'utf8',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
generate(cwd, {
|
|
45
|
+
apiUrl: 'https://example.test',
|
|
46
|
+
apiToken: 'secret-token',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
50
|
+
assert.equal(config.SessionStart.length, 2);
|
|
51
|
+
assert.match(config.SessionStart[0].hooks[0].prompt, /Existing hook/);
|
|
52
|
+
assert.match(config.SessionStart[1].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
53
|
+
});
|
|
@@ -1,28 +1,83 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CTM_SYNC_AGENT_REL_PATH,
|
|
5
|
+
GUARDRAILS_PROFILER_SKILL_REL_DIR,
|
|
6
|
+
GUARDRAILS_SELECTION_SKILL_REL_DIR,
|
|
7
|
+
THREAT_MODELLING_SKILL_REL_DIR,
|
|
8
|
+
} from '../../utils/constants.js';
|
|
3
9
|
import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
4
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getCtmSyncWorkflowContent,
|
|
12
|
+
getGuardrailsInitProfileContent,
|
|
13
|
+
getRuleContent,
|
|
14
|
+
getThreatModellingSkillContent,
|
|
15
|
+
} from './content.js';
|
|
16
|
+
|
|
17
|
+
function writeGeneratedText(filePath, content) {
|
|
18
|
+
const action = existsSync(filePath) ? 'updated' : 'created';
|
|
19
|
+
writeText(filePath, content);
|
|
20
|
+
return { filePath, action };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getAgentBody(content) {
|
|
24
|
+
return content.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getClaudeCtmSyncAgentContent(options = {}) {
|
|
28
|
+
const body = getAgentBody(getCtmSyncWorkflowContent(options));
|
|
29
|
+
return [
|
|
30
|
+
'---',
|
|
31
|
+
'name: ctm_sync',
|
|
32
|
+
'description: Use this agent after security-relevant implementation or threat modelling work to synchronize the latest threat model, mitigations, and guardrails to SRAI.',
|
|
33
|
+
'model: inherit',
|
|
34
|
+
'color: blue',
|
|
35
|
+
'---',
|
|
36
|
+
'',
|
|
37
|
+
body,
|
|
38
|
+
'',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
5
41
|
|
|
6
42
|
/**
|
|
7
43
|
* Generate Claude Code workspace rule — appends to CLAUDE.md
|
|
8
44
|
*/
|
|
9
45
|
export function generate(cwd, options = {}) {
|
|
10
|
-
const
|
|
11
|
-
const content = getRuleContent({
|
|
46
|
+
const optionsWithSkillDirs = {
|
|
12
47
|
...options,
|
|
13
48
|
guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.claude,
|
|
14
|
-
|
|
49
|
+
threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.claude,
|
|
50
|
+
ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.claude,
|
|
51
|
+
};
|
|
52
|
+
const filePath = join(cwd, 'CLAUDE.md');
|
|
53
|
+
const content = getRuleContent(optionsWithSkillDirs);
|
|
15
54
|
const action = upsertSentinelBlock(filePath, content);
|
|
16
55
|
|
|
56
|
+
const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.claude, 'SKILL.md');
|
|
57
|
+
const threatSkill = writeGeneratedText(
|
|
58
|
+
threatSkillPath,
|
|
59
|
+
getThreatModellingSkillContent(optionsWithSkillDirs),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const ctmSyncAgentPath = join(cwd, CTM_SYNC_AGENT_REL_PATH.claude);
|
|
63
|
+
const ctmSyncAgent = writeGeneratedText(
|
|
64
|
+
ctmSyncAgentPath,
|
|
65
|
+
getClaudeCtmSyncAgentContent(optionsWithSkillDirs),
|
|
66
|
+
);
|
|
67
|
+
|
|
17
68
|
const guardrailsInitPath = join(cwd, '.claude', 'commands', 'guardrails-init-profile.md');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
69
|
+
const guardrailsInit = writeGeneratedText(
|
|
70
|
+
guardrailsInitPath,
|
|
71
|
+
getGuardrailsInitProfileContent({
|
|
72
|
+
...optionsWithSkillDirs,
|
|
73
|
+
guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.claude,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
23
76
|
|
|
24
77
|
return [
|
|
25
78
|
{ filePath, action, kind: 'rule' },
|
|
26
|
-
{
|
|
79
|
+
{ ...threatSkill, kind: 'skill' },
|
|
80
|
+
{ ...ctmSyncAgent, kind: 'agent' },
|
|
81
|
+
{ ...guardrailsInit, kind: 'command' },
|
|
27
82
|
];
|
|
28
83
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync } 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 { generate } from './claude.js';
|
|
7
|
+
|
|
8
|
+
test('Claude generator writes CLAUDE.md, threat skill, ctm_sync agent, and profiling command', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-'));
|
|
10
|
+
|
|
11
|
+
const results = generate(cwd, { projectName: 'SmokeProject' });
|
|
12
|
+
|
|
13
|
+
const expectedPaths = [
|
|
14
|
+
'CLAUDE.md',
|
|
15
|
+
'.claude/skills/threat-modelling/SKILL.md',
|
|
16
|
+
'.claude/agents/ctm_sync.md',
|
|
17
|
+
'.claude/commands/guardrails-init-profile.md',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const relPath of expectedPaths) {
|
|
21
|
+
assert.equal(existsSync(join(cwd, relPath)), true, `${relPath} should exist`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'agent', 'command']);
|
|
25
|
+
|
|
26
|
+
const instructions = readFileSync(join(cwd, 'CLAUDE.md'), 'utf8');
|
|
27
|
+
assert.match(instructions, /\.claude\/skills\/guardrails-selection\/SKILL\.md/);
|
|
28
|
+
assert.match(instructions, /\.claude\/skills\/threat-modelling\/SKILL\.md/);
|
|
29
|
+
assert.match(instructions, /\.claude\/agents\/ctm_sync\.md/);
|
|
30
|
+
|
|
31
|
+
const threatSkill = readFileSync(join(cwd, '.claude/skills/threat-modelling/SKILL.md'), 'utf8');
|
|
32
|
+
assert.match(threatSkill, /\.claude\/agents\/ctm_sync\.md/);
|
|
33
|
+
assert.doesNotMatch(threatSkill, /get_project_profile_description/);
|
|
34
|
+
|
|
35
|
+
const agent = readFileSync(join(cwd, '.claude/agents/ctm_sync.md'), 'utf8');
|
|
36
|
+
assert.match(agent, /^---\nname: ctm_sync/m);
|
|
37
|
+
assert.match(agent, /model: inherit/);
|
|
38
|
+
assert.match(agent, /Configured SRAI project name: `SmokeProject`/);
|
|
39
|
+
});
|
|
@@ -26,20 +26,26 @@ function getCodexSessionHookContent() {
|
|
|
26
26
|
'',
|
|
27
27
|
'For any request with security impact (auth, authorization, crypto, input handling, secrets, network, data storage, dependencies, new APIs/endpoints, infrastructure, or code handling untrusted data), follow this order:',
|
|
28
28
|
'',
|
|
29
|
-
'1. Fetch Vibe Guardrails first using .
|
|
30
|
-
'2. Run PWNISMS threat modelling using .
|
|
29
|
+
'1. Fetch Vibe Guardrails first using .codex/skills/guardrails-selection/SKILL.md. Resolve the project, call get_guardrails, shortlist relevant guardrails, then call get_guardrail_by_id for the shortlist.',
|
|
30
|
+
'2. Run PWNISMS threat modelling using .codex/skills/threat-modelling/SKILL.md. Explicitly walk Product, Workload, Network, IAM, Secrets, Monitoring, and Supply Chain.',
|
|
31
31
|
'3. Implement secure code using both the hydrated guardrails and PWNISMS findings.',
|
|
32
32
|
'4. Invoke the ctm_sync subagent using .codex/agents/ctm_sync.toml after threat modelling or guardrail enforcement. Reuse the exact guardrail shortlist; do not re-query guardrails during CTM sync.',
|
|
33
33
|
'',
|
|
34
34
|
'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, ctm_sync last.',
|
|
35
35
|
].join('\n');
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const sessionStartPayload = JSON.stringify({
|
|
38
38
|
hookSpecificOutput: {
|
|
39
39
|
hookEventName: 'SessionStart',
|
|
40
40
|
additionalContext,
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
|
+
const userPromptPayload = JSON.stringify({
|
|
44
|
+
hookSpecificOutput: {
|
|
45
|
+
hookEventName: 'UserPromptSubmit',
|
|
46
|
+
additionalContext,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
43
49
|
|
|
44
50
|
return JSON.stringify(
|
|
45
51
|
{
|
|
@@ -50,13 +56,25 @@ function getCodexSessionHookContent() {
|
|
|
50
56
|
hooks: [
|
|
51
57
|
{
|
|
52
58
|
type: 'command',
|
|
53
|
-
command: `printf '%s\\n' '${
|
|
59
|
+
command: `printf '%s\\n' '${sessionStartPayload.replaceAll("'", "'\\''")}'`,
|
|
54
60
|
timeout: 5,
|
|
55
61
|
statusMessage: 'Loading SRAI security session policy',
|
|
56
62
|
},
|
|
57
63
|
],
|
|
58
64
|
},
|
|
59
65
|
],
|
|
66
|
+
UserPromptSubmit: [
|
|
67
|
+
{
|
|
68
|
+
hooks: [
|
|
69
|
+
{
|
|
70
|
+
type: 'command',
|
|
71
|
+
command: `printf '%s\\n' '${userPromptPayload.replaceAll("'", "'\\''")}'`,
|
|
72
|
+
timeout: 5,
|
|
73
|
+
statusMessage: 'Refreshing SRAI security session policy',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
60
78
|
},
|
|
61
79
|
},
|
|
62
80
|
null,
|
|
@@ -90,7 +108,7 @@ export function generate(cwd, options = {}) {
|
|
|
90
108
|
threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.codex,
|
|
91
109
|
ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.codex,
|
|
92
110
|
};
|
|
93
|
-
const filePath = join(cwd, 'AGENTS.md');
|
|
111
|
+
const filePath = join(cwd, '.codex', 'AGENTS.md');
|
|
94
112
|
const content = getRuleContent(optionsWithSkillDirs);
|
|
95
113
|
const action = upsertSentinelBlock(filePath, content);
|
|
96
114
|
|
|
@@ -11,8 +11,8 @@ test('Codex generator writes AGENTS, skills, subagent, hooks, and profiling comm
|
|
|
11
11
|
const results = generate(cwd, { projectName: 'SmokeProject' });
|
|
12
12
|
|
|
13
13
|
const expectedPaths = [
|
|
14
|
-
'AGENTS.md',
|
|
15
|
-
'.
|
|
14
|
+
'.codex/AGENTS.md',
|
|
15
|
+
'.codex/skills/threat-modelling/SKILL.md',
|
|
16
16
|
'.codex/agents/ctm_sync.toml',
|
|
17
17
|
'.codex/hooks.json',
|
|
18
18
|
'.codex/commands/guardrails-init-profile.md',
|
|
@@ -24,13 +24,14 @@ test('Codex generator writes AGENTS, skills, subagent, hooks, and profiling comm
|
|
|
24
24
|
|
|
25
25
|
assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'agent', 'hooks', 'command']);
|
|
26
26
|
|
|
27
|
-
const instructions = readFileSync(join(cwd, 'AGENTS.md'), 'utf8');
|
|
28
|
-
assert.match(instructions, /\.
|
|
29
|
-
assert.match(instructions, /\.
|
|
27
|
+
const instructions = readFileSync(join(cwd, '.codex/AGENTS.md'), 'utf8');
|
|
28
|
+
assert.match(instructions, /\.codex\/skills\/guardrails-selection\/SKILL\.md/);
|
|
29
|
+
assert.match(instructions, /\.codex\/skills\/threat-modelling\/SKILL\.md/);
|
|
30
30
|
assert.match(instructions, /\.codex\/agents\/ctm_sync\.toml/);
|
|
31
31
|
assert.doesNotMatch(instructions, /\.cursor/);
|
|
32
|
+
assert.doesNotMatch(instructions, /\.agents\/skills/);
|
|
32
33
|
|
|
33
|
-
const threatSkill = readFileSync(join(cwd, '.
|
|
34
|
+
const threatSkill = readFileSync(join(cwd, '.codex/skills/threat-modelling/SKILL.md'), 'utf8');
|
|
34
35
|
for (const heading of ['Product', 'Workload', 'Network', 'IAM', 'Secrets', 'Monitoring', 'Supply Chain']) {
|
|
35
36
|
assert.match(threatSkill, new RegExp(heading));
|
|
36
37
|
}
|
|
@@ -44,4 +45,6 @@ test('Codex generator writes AGENTS, skills, subagent, hooks, and profiling comm
|
|
|
44
45
|
|
|
45
46
|
const hooks = JSON.parse(readFileSync(join(cwd, '.codex/hooks.json'), 'utf8'));
|
|
46
47
|
assert.equal(hooks.hooks.SessionStart[0].hooks[0].type, 'command');
|
|
48
|
+
assert.equal(hooks.hooks.UserPromptSubmit[0].hooks[0].type, 'command');
|
|
49
|
+
assert.match(hooks.hooks.UserPromptSubmit[0].hooks[0].command, /"hookEventName":"UserPromptSubmit"/);
|
|
47
50
|
});
|
|
@@ -31,6 +31,14 @@ During `securityreview-kit init`, choose **Yes** when asked to run Cursor login
|
|
|
31
31
|
|
|
32
32
|
You can still sign in manually with `agent login` (or `cursor-agent login`). To handle trust/login interactively in the terminal, omit `--trust` and `--approve-mcps`.
|
|
33
33
|
|
|
34
|
+
## Claude Code CLI (scripted)
|
|
35
|
+
|
|
36
|
+
From the repo root, non-interactive runs should execute with the project settings file and the Haiku model for this profiling pass:
|
|
37
|
+
|
|
38
|
+
`claude -p "<your profiling instructions>" --settings .claude/settings.json --model haiku`
|
|
39
|
+
|
|
40
|
+
During `securityreview-kit init`, choose **Yes** when asked to run Claude Code login, or pass **`--profiler-claude-login`** with **`--profile-repo`** so `claude auth login` and profiling stay in one run.
|
|
41
|
+
|
|
34
42
|
## GitHub Copilot CLI (scripted)
|
|
35
43
|
|
|
36
44
|
From the repo root, non-interactive runs should load the SRAI MCP server and allow the tools needed to scan, write profile files, and call MCP:
|
|
@@ -38,3 +46,9 @@ From the repo root, non-interactive runs should load the SRAI MCP server and all
|
|
|
38
46
|
`copilot -p "<your profiling instructions>" --additional-mcp-config '{"mcpServers":{"security-review-mcp":{"type":"stdio","command":"npx","args":["-y","@securityreviewai/security-review-mcp@latest"]}}}' --allow-all`
|
|
39
47
|
|
|
40
48
|
During `securityreview-kit init`, choose **Yes** when asked to run GitHub Copilot CLI login, or pass **`--profiler-copilot-login`** with **`--profile-repo`** so `copilot login` and profiling stay in one run.
|
|
49
|
+
|
|
50
|
+
## Codex CLI (scripted)
|
|
51
|
+
|
|
52
|
+
From the repo root, non-interactive runs should execute via `codex exec` and include the SRAI MCP server configuration for that run.
|
|
53
|
+
|
|
54
|
+
During `securityreview-kit init`, choose **Yes** when asked to run Codex login, or pass **`--profiler-codex-login`** with **`--profile-repo`** so `codex login --device-auth` and profiling stay in one run.
|
|
@@ -11,7 +11,7 @@ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
|
|
|
11
11
|
|
|
12
12
|
## Canonical paths
|
|
13
13
|
|
|
14
|
-
- **This skill & signal registry (read-only):** `<GUARDRAILS_SKILL_DIR>/` — e.g. `.cursor/skills/guardrails-profiler`, `.github/skills/guardrails-profiler`, `.claude/skills/guardrails-profiler`, or `.
|
|
14
|
+
- **This skill & signal registry (read-only):** `<GUARDRAILS_SKILL_DIR>/` — e.g. `.cursor/skills/guardrails-profiler`, `.github/skills/guardrails-profiler`, `.claude/skills/guardrails-profiler`, or `.codex/skills/guardrails-profiler` for Codex depending on where this file was installed.
|
|
15
15
|
- **Signal registry file:** `<GUARDRAILS_SKILL_DIR>/references/signal-registry.json`
|
|
16
16
|
- **Local guardrails file:** `.guardrails/profile.json`
|
|
17
17
|
- **Combined manifest (project root):** `profile.json` — includes guardrails profile, vibe profile fields for MCP, and default pack payload
|
package/src/utils/constants.js
CHANGED
|
@@ -38,7 +38,7 @@ export const TARGETS = {
|
|
|
38
38
|
codex: {
|
|
39
39
|
name: 'Codex',
|
|
40
40
|
mcpConfigPath: '.codex/config.toml',
|
|
41
|
-
rulePath: 'AGENTS.md',
|
|
41
|
+
rulePath: '.codex/AGENTS.md',
|
|
42
42
|
ruleMode: 'append',
|
|
43
43
|
detectDirs: ['.codex'],
|
|
44
44
|
},
|
|
@@ -64,7 +64,7 @@ export const GUARDRAILS_PROFILER_SKILL_REL_DIR = {
|
|
|
64
64
|
cursor: '.cursor/skills/guardrails-profiler',
|
|
65
65
|
claude: '.claude/skills/guardrails-profiler',
|
|
66
66
|
vscode: '.github/skills/guardrails-profiler',
|
|
67
|
-
codex: '.
|
|
67
|
+
codex: '.codex/skills/guardrails-profiler',
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
/** Relative workspace dirs for the guardrails-selection skill (per IDE / CLI). */
|
|
@@ -72,19 +72,21 @@ export const GUARDRAILS_SELECTION_SKILL_REL_DIR = {
|
|
|
72
72
|
cursor: '.cursor/skills/guardrails-selection',
|
|
73
73
|
claude: '.claude/skills/guardrails-selection',
|
|
74
74
|
vscode: '.github/skills/guardrails-selection',
|
|
75
|
-
codex: '.
|
|
75
|
+
codex: '.codex/skills/guardrails-selection',
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
/** Relative workspace dirs for the PWNISMS threat-modelling skill (per IDE / CLI). */
|
|
79
79
|
export const THREAT_MODELLING_SKILL_REL_DIR = {
|
|
80
80
|
cursor: '.cursor/skills/threat-modelling',
|
|
81
|
+
claude: '.claude/skills/threat-modelling',
|
|
81
82
|
vscode: '.github/skills/threat-modelling',
|
|
82
|
-
codex: '.
|
|
83
|
+
codex: '.codex/skills/threat-modelling',
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
/** Relative workspace paths for the CTM sync agent/workflow (per IDE / CLI). */
|
|
86
87
|
export const CTM_SYNC_AGENT_REL_PATH = {
|
|
87
88
|
cursor: '.cursor/agents/ctm_sync.md',
|
|
89
|
+
claude: '.claude/agents/ctm_sync.md',
|
|
88
90
|
vscode: '.github/agents/ctm_sync.agent.md',
|
|
89
91
|
codex: '.codex/agents/ctm_sync.toml',
|
|
90
92
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { GUARDRAILS_PROFILER_SKILL_REL_DIR, MCP_SERVER_NAME } from './constants.js';
|
|
3
|
+
import { GUARDRAILS_PROFILER_SKILL_REL_DIR, MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from './constants.js';
|
|
4
4
|
import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
|
|
5
|
-
import { readJson } from './fs-helpers.js';
|
|
5
|
+
import { readJson, readText, writeText } from './fs-helpers.js';
|
|
6
6
|
|
|
7
7
|
const PREFERRED_ORDER = ['cursor', 'vscode', 'claude', 'codex'];
|
|
8
8
|
|
|
@@ -11,6 +11,43 @@ function commandOk(cmd, args = ['--version'], env = process.env) {
|
|
|
11
11
|
return r.status === 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function getProfilerLogPath(cwd, target) {
|
|
15
|
+
return join(cwd, '.guardrails', 'logs', `profiler-${target}.log`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildProfilerEnv(target, streamProgress) {
|
|
19
|
+
const env = augmentPathEnv(process.env);
|
|
20
|
+
if (target === 'codex' && streamProgress && !env.RUST_LOG) {
|
|
21
|
+
env.RUST_LOG = 'info';
|
|
22
|
+
}
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function persistProfilerLog(cwd, target, result) {
|
|
27
|
+
if (!result || (result.stdout == null && result.stderr == null)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const stdout = String(result.stdout || '');
|
|
32
|
+
const stderr = String(result.stderr || '');
|
|
33
|
+
const body = [
|
|
34
|
+
`target=${target}`,
|
|
35
|
+
`status=${result.status ?? ''}`,
|
|
36
|
+
`signal=${result.signal ?? ''}`,
|
|
37
|
+
'',
|
|
38
|
+
'--- stdout ---',
|
|
39
|
+
stdout,
|
|
40
|
+
'',
|
|
41
|
+
'--- stderr ---',
|
|
42
|
+
stderr,
|
|
43
|
+
'',
|
|
44
|
+
].join('\n');
|
|
45
|
+
|
|
46
|
+
const logPath = getProfilerLogPath(cwd, target);
|
|
47
|
+
writeText(logPath, body);
|
|
48
|
+
return logPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
export function buildProfilerAgentPrompt(projectName, agentTarget = 'cursor') {
|
|
15
52
|
const p = String(projectName || '').trim() || '<SRAI_PROJECT_NAME>';
|
|
16
53
|
const skillRoot =
|
|
@@ -72,6 +109,75 @@ export function runCopilotLogin(cwd) {
|
|
|
72
109
|
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
73
110
|
}
|
|
74
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Run Claude Code login in the current terminal.
|
|
114
|
+
*/
|
|
115
|
+
export function runClaudeLogin(cwd) {
|
|
116
|
+
const env = augmentPathEnv(process.env);
|
|
117
|
+
if (!commandOk('claude', ['--version'], env)) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
status: null,
|
|
121
|
+
message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const r = spawnSync('claude', ['auth', 'login', '--claudeai'], { cwd, stdio: 'inherit', env });
|
|
125
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
126
|
+
if (r.status === null && spawnErr) {
|
|
127
|
+
return { ok: false, status: null, message: spawnErr };
|
|
128
|
+
}
|
|
129
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run Codex login in the current terminal. Device auth keeps the flow in-terminal.
|
|
134
|
+
*/
|
|
135
|
+
export function runCodexLogin(cwd) {
|
|
136
|
+
const env = augmentPathEnv(process.env);
|
|
137
|
+
if (!commandOk('codex', ['--version'], env)) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
status: null,
|
|
141
|
+
message: 'Codex CLI not found (`codex`). Install with `npm install -g @openai/codex`.',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const r = spawnSync('codex', ['login', '--device-auth'], { cwd, stdio: 'inherit', env });
|
|
145
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
146
|
+
if (r.status === null && spawnErr) {
|
|
147
|
+
return { ok: false, status: null, message: spawnErr };
|
|
148
|
+
}
|
|
149
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function escapeTomlString(value) {
|
|
153
|
+
return String(value || '').replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readCodexMcpEnv(cwd) {
|
|
157
|
+
const config = readText(join(cwd, '.codex', 'config.toml'));
|
|
158
|
+
const apiUrl = config.match(/^\s*SECURITY_REVIEW_API_URL\s*=\s*"([^"]*)"/m)?.[1] || '';
|
|
159
|
+
const apiToken = config.match(/^\s*SECURITY_REVIEW_API_TOKEN\s*=\s*"([^"]*)"/m)?.[1] || '';
|
|
160
|
+
return { apiUrl, apiToken };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function buildCodexConfigOverrides(cwd, overrides = {}) {
|
|
164
|
+
const fileValues = readCodexMcpEnv(cwd);
|
|
165
|
+
const apiUrl = String(overrides.apiUrl || fileValues.apiUrl || '').trim();
|
|
166
|
+
const apiToken = String(overrides.apiToken || fileValues.apiToken || '').trim();
|
|
167
|
+
|
|
168
|
+
if (!apiUrl || !apiToken) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [
|
|
173
|
+
'features.codex_hooks=true',
|
|
174
|
+
`mcp_servers.${MCP_SERVER_NAME}.command="npx"`,
|
|
175
|
+
`mcp_servers.${MCP_SERVER_NAME}.args=["-y","${MCP_SERVER_PACKAGE}@latest"]`,
|
|
176
|
+
`mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_URL="${escapeTomlString(apiUrl)}"`,
|
|
177
|
+
`mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_TOKEN="${escapeTomlString(apiToken)}"`,
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
75
181
|
export function pickProfilerAgentTarget(targets) {
|
|
76
182
|
for (const t of PREFERRED_ORDER) {
|
|
77
183
|
if (targets.includes(t)) {
|
|
@@ -94,10 +200,18 @@ export function pickProfilerAgentTarget(targets) {
|
|
|
94
200
|
*/
|
|
95
201
|
export function runProfilerAgent(
|
|
96
202
|
cwd,
|
|
97
|
-
{
|
|
203
|
+
{
|
|
204
|
+
target,
|
|
205
|
+
projectName,
|
|
206
|
+
apiUrl,
|
|
207
|
+
apiToken,
|
|
208
|
+
cursorTrust = true,
|
|
209
|
+
streamProgress = false,
|
|
210
|
+
showOutput = streamProgress,
|
|
211
|
+
},
|
|
98
212
|
) {
|
|
99
213
|
const prompt = buildProfilerAgentPrompt(projectName, target);
|
|
100
|
-
const env =
|
|
214
|
+
const env = buildProfilerEnv(target, streamProgress);
|
|
101
215
|
const opts = showOutput
|
|
102
216
|
? { cwd, stdio: 'inherit', env }
|
|
103
217
|
: { cwd, stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 };
|
|
@@ -129,27 +243,45 @@ export function runProfilerAgent(
|
|
|
129
243
|
if (r.error) {
|
|
130
244
|
return { ok: false, message: r.error.message };
|
|
131
245
|
}
|
|
132
|
-
return buildProfilerResult(r);
|
|
246
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
133
247
|
}
|
|
134
248
|
|
|
135
249
|
if (target === 'claude') {
|
|
136
250
|
if (!commandOk('claude', ['--version'], env)) {
|
|
137
251
|
return { ok: false, message: 'claude not on PATH' };
|
|
138
252
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
253
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
254
|
+
const args = ['-p', '--settings', settingsPath, '--model', 'haiku'];
|
|
255
|
+
if (streamProgress) {
|
|
256
|
+
args.push('--output-format', 'stream-json', '--include-partial-messages', '--include-hook-events', '--verbose');
|
|
257
|
+
}
|
|
258
|
+
args.push(prompt);
|
|
142
259
|
const r = spawnSync('claude', args, opts);
|
|
143
|
-
return buildProfilerResult(r);
|
|
260
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
144
261
|
}
|
|
145
262
|
|
|
146
263
|
if (target === 'codex') {
|
|
147
264
|
if (!commandOk('codex', ['--version'], env)) {
|
|
148
265
|
return { ok: false, message: 'codex not on PATH' };
|
|
149
266
|
}
|
|
150
|
-
const
|
|
267
|
+
const configOverrides = buildCodexConfigOverrides(cwd, { apiUrl, apiToken });
|
|
268
|
+
if (!configOverrides) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
message:
|
|
272
|
+
'Codex profiling needs SRAI MCP credentials. Re-run init with Codex selected and MCP installation enabled.',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const args = ['exec', '--full-auto'];
|
|
276
|
+
for (const override of configOverrides) {
|
|
277
|
+
args.push('-c', override);
|
|
278
|
+
}
|
|
279
|
+
if (streamProgress) {
|
|
280
|
+
args.push('--json');
|
|
281
|
+
}
|
|
282
|
+
args.push(prompt);
|
|
151
283
|
const r = spawnSync('codex', args, opts);
|
|
152
|
-
return buildProfilerResult(r);
|
|
284
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
153
285
|
}
|
|
154
286
|
|
|
155
287
|
if (target === 'vscode') {
|
|
@@ -177,7 +309,7 @@ export function runProfilerAgent(
|
|
|
177
309
|
args.push('--output-format', 'json');
|
|
178
310
|
}
|
|
179
311
|
const r = spawnSync('copilot', args, opts);
|
|
180
|
-
return buildProfilerResult(r);
|
|
312
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
181
313
|
}
|
|
182
314
|
|
|
183
315
|
return { ok: false, message: 'unsupported agent target' };
|
|
@@ -207,13 +339,13 @@ export function buildCopilotAdditionalMcpConfig(cwd) {
|
|
|
207
339
|
});
|
|
208
340
|
}
|
|
209
341
|
|
|
210
|
-
function buildProfilerResult(result) {
|
|
342
|
+
function buildProfilerResult(result, extras = {}) {
|
|
211
343
|
if (result.error) {
|
|
212
|
-
return { ok: false, status: result.status, message: result.error.message };
|
|
344
|
+
return { ok: false, status: result.status, message: result.error.message, ...extras };
|
|
213
345
|
}
|
|
214
346
|
|
|
215
347
|
if (result.status === 0) {
|
|
216
|
-
return { ok: true, status: result.status };
|
|
348
|
+
return { ok: true, status: result.status, ...extras };
|
|
217
349
|
}
|
|
218
350
|
|
|
219
351
|
const outputTail = summarizeProfilerOutput(result.stderr || result.stdout || '');
|
|
@@ -228,6 +360,7 @@ function buildProfilerResult(result) {
|
|
|
228
360
|
ok: false,
|
|
229
361
|
status: result.status,
|
|
230
362
|
message: outputTail ? `${exitDetail}; last output: ${outputTail}` : exitDetail,
|
|
363
|
+
...extras,
|
|
231
364
|
};
|
|
232
365
|
}
|
|
233
366
|
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { test } from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import {
|
|
7
|
+
buildCodexConfigOverrides,
|
|
7
8
|
buildCopilotAdditionalMcpConfig,
|
|
8
9
|
pickProfilerAgentTarget,
|
|
9
10
|
} from './profiler-agent.js';
|
|
@@ -39,3 +40,34 @@ test('buildCopilotAdditionalMcpConfig converts VS Code MCP config', () => {
|
|
|
39
40
|
assert.equal(converted.mcpServers['security-review-mcp'].command, 'npx');
|
|
40
41
|
assert.deepEqual(converted.mcpServers['security-review-mcp'].tools, ['*']);
|
|
41
42
|
});
|
|
43
|
+
|
|
44
|
+
test('buildCodexConfigOverrides builds inline MCP config for profiling', () => {
|
|
45
|
+
const cwd = join(tmpdir(), `securityreview-kit-codex-${Date.now()}`);
|
|
46
|
+
mkdirSync(join(cwd, '.codex'), { recursive: true });
|
|
47
|
+
writeFileSync(
|
|
48
|
+
join(cwd, '.codex', 'config.toml'),
|
|
49
|
+
[
|
|
50
|
+
'[features]',
|
|
51
|
+
'codex_hooks = true',
|
|
52
|
+
'',
|
|
53
|
+
'[mcp_servers.security-review-mcp]',
|
|
54
|
+
'command = "npx"',
|
|
55
|
+
'args = ["-y", "@securityreviewai/security-review-mcp@latest"]',
|
|
56
|
+
'',
|
|
57
|
+
'[mcp_servers.security-review-mcp.env]',
|
|
58
|
+
'SECURITY_REVIEW_API_URL = "https://api.example.test"',
|
|
59
|
+
'SECURITY_REVIEW_API_TOKEN = "token"',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n'),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const overrides = buildCodexConfigOverrides(cwd);
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(overrides, [
|
|
67
|
+
'features.codex_hooks=true',
|
|
68
|
+
'mcp_servers.security-review-mcp.command="npx"',
|
|
69
|
+
'mcp_servers.security-review-mcp.args=["-y","@securityreviewai/security-review-mcp@latest"]',
|
|
70
|
+
'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_URL="https://api.example.test"',
|
|
71
|
+
'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_TOKEN="token"',
|
|
72
|
+
]);
|
|
73
|
+
});
|