@securityreviewai/securityreview-kit 0.1.37 → 0.1.38
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 +4 -1
- package/package.json +1 -1
- package/src/cli.js +8 -0
- package/src/commands/init.js +62 -3
- 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 +6 -0
- package/src/generators/rules/guardrails-profiler/SKILL.md +1 -1
- package/src/utils/constants.js +4 -4
- package/src/utils/profiler-agent.js +122 -12
- package/src/utils/profiler-agent.test.js +32 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ npx @securityreviewai/securityreview-kit init --switch-project
|
|
|
31
31
|
| Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.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
|
|
|
@@ -54,6 +54,9 @@ Options:
|
|
|
54
54
|
--profile-repo Run the guardrails profiler after init
|
|
55
55
|
--profiler-copilot-login
|
|
56
56
|
Run GitHub Copilot CLI login before VS Code Copilot profiling
|
|
57
|
+
--profiler-codex-login Run Codex login before Codex profiling
|
|
58
|
+
--profiler-verbose Show live profiler output while profiling runs
|
|
59
|
+
--show-profiler-logs Alias for --profiler-verbose
|
|
57
60
|
```
|
|
58
61
|
|
|
59
62
|
### `@securityreviewai/securityreview-kit init --switch-project`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -41,6 +41,10 @@ export function run() {
|
|
|
41
41
|
'--profiler-copilot-login',
|
|
42
42
|
'Before VS Code Copilot profiling, run `copilot login` in this terminal',
|
|
43
43
|
)
|
|
44
|
+
.option(
|
|
45
|
+
'--profiler-codex-login',
|
|
46
|
+
'Before Codex profiling, run `codex login --device-auth` in this terminal',
|
|
47
|
+
)
|
|
44
48
|
.option(
|
|
45
49
|
'--profiler-quiet',
|
|
46
50
|
'When profiling, use the standard progress message (default; retained for compatibility)',
|
|
@@ -49,6 +53,10 @@ export function run() {
|
|
|
49
53
|
'--profiler-verbose',
|
|
50
54
|
'When profiling, show live agent output for troubleshooting',
|
|
51
55
|
)
|
|
56
|
+
.option(
|
|
57
|
+
'--show-profiler-logs',
|
|
58
|
+
'Alias for --profiler-verbose; show live profiler logs while profiling runs',
|
|
59
|
+
)
|
|
52
60
|
.action(async (options) => {
|
|
53
61
|
try {
|
|
54
62
|
if (options.switchProject) {
|
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ 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
|
+
runCodexLogin,
|
|
9
10
|
runCopilotLogin,
|
|
10
11
|
runCursorAgentLogin,
|
|
11
12
|
runProfilerAgent,
|
|
@@ -485,27 +486,64 @@ export async function initCommand(options) {
|
|
|
485
486
|
}
|
|
486
487
|
console.log('');
|
|
487
488
|
}
|
|
489
|
+
} else if (agentTarget === 'codex') {
|
|
490
|
+
console.log(
|
|
491
|
+
chalk.dim(
|
|
492
|
+
' Codex: profiling passes the SRAI MCP server directly to `codex exec` for this run.',
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
let runLogin = Boolean(options.profilerCodexLogin);
|
|
497
|
+
if (!runLogin && interactive) {
|
|
498
|
+
runLogin = await confirm({
|
|
499
|
+
message:
|
|
500
|
+
'Run Codex login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
|
|
501
|
+
default: true,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (runLogin) {
|
|
505
|
+
console.log('');
|
|
506
|
+
console.log(chalk.bold.white(' Codex login'));
|
|
507
|
+
console.log(chalk.dim(' Complete the device-code prompt, then return here.\n'));
|
|
508
|
+
const loginResult = runCodexLogin(cwd);
|
|
509
|
+
if (loginResult.ok) {
|
|
510
|
+
console.log(chalk.green(' \u2713 Codex login step finished.'));
|
|
511
|
+
} else {
|
|
512
|
+
console.log(
|
|
513
|
+
chalk.yellow(
|
|
514
|
+
` \u26a0 Codex login exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; sign in and re-run init if it fails.`,
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
console.log('');
|
|
519
|
+
}
|
|
488
520
|
} else {
|
|
489
521
|
console.log(chalk.dim(' (Sign-in or approvals may be required in your terminal.)'));
|
|
490
522
|
}
|
|
491
523
|
console.log('');
|
|
524
|
+
const profilerVerbose = Boolean(options.profilerVerbose || options.showProfilerLogs);
|
|
492
525
|
const showProfilerOutput = Boolean(
|
|
493
|
-
|
|
526
|
+
profilerVerbose || (agentTarget === 'cursor' && options.profilerNoTrust),
|
|
494
527
|
);
|
|
495
528
|
if (showProfilerOutput) {
|
|
496
|
-
console.log(chalk.dim(' Profiling in progress.
|
|
529
|
+
console.log(chalk.dim(' Profiling in progress. Live profiler logs are visible for this run...'));
|
|
497
530
|
} else {
|
|
498
531
|
console.log(chalk.dim(' Profiling in progress. This can take a few minutes...'));
|
|
499
532
|
}
|
|
500
533
|
const pr = runProfilerAgent(cwd, {
|
|
501
534
|
target: agentTarget,
|
|
502
535
|
projectName: projectNameForSkill,
|
|
536
|
+
apiUrl: envVars?.apiUrl,
|
|
537
|
+
apiToken: envVars?.apiToken,
|
|
503
538
|
cursorTrust: !options.profilerNoTrust,
|
|
504
|
-
streamProgress:
|
|
539
|
+
streamProgress: profilerVerbose,
|
|
505
540
|
showOutput: showProfilerOutput,
|
|
506
541
|
});
|
|
507
542
|
if (pr.ok) {
|
|
508
543
|
console.log(chalk.green(' \u2713 Profiler agent finished.'));
|
|
544
|
+
if (pr.logPath) {
|
|
545
|
+
console.log(chalk.dim(` Profiler log saved to ${pr.logPath}`));
|
|
546
|
+
}
|
|
509
547
|
} else {
|
|
510
548
|
const detail =
|
|
511
549
|
pr.message ||
|
|
@@ -515,6 +553,9 @@ export async function initCommand(options) {
|
|
|
515
553
|
` \u26a0 Profiler agent exited with an error: ${detail}. You can run the guardrails-init-profile workflow manually.`,
|
|
516
554
|
),
|
|
517
555
|
);
|
|
556
|
+
if (pr.logPath) {
|
|
557
|
+
console.log(chalk.dim(` Profiler log saved to ${pr.logPath}`));
|
|
558
|
+
}
|
|
518
559
|
if (agentTarget === 'cursor') {
|
|
519
560
|
console.log('');
|
|
520
561
|
console.log(chalk.dim(' Typical fixes:'));
|
|
@@ -551,6 +592,24 @@ export async function initCommand(options) {
|
|
|
551
592
|
' • MCP missing: re-run init with VS Code selected and MCP installation enabled so `.vscode/mcp.json` is written.',
|
|
552
593
|
),
|
|
553
594
|
);
|
|
595
|
+
} else if (agentTarget === 'codex') {
|
|
596
|
+
console.log('');
|
|
597
|
+
console.log(chalk.dim(' Typical fixes:'));
|
|
598
|
+
console.log(
|
|
599
|
+
chalk.dim(
|
|
600
|
+
' • Not signed in: re-run `securityreview-kit init` and choose Yes for Codex login, or pass `--profiler-codex-login` with `--profile-repo`.',
|
|
601
|
+
),
|
|
602
|
+
);
|
|
603
|
+
console.log(
|
|
604
|
+
chalk.dim(
|
|
605
|
+
' • CLI missing: install Codex CLI and verify `codex --version`.',
|
|
606
|
+
),
|
|
607
|
+
);
|
|
608
|
+
console.log(
|
|
609
|
+
chalk.dim(
|
|
610
|
+
' • MCP missing: re-run init with Codex selected and MCP installation enabled so the SRAI credentials are available for profiling.',
|
|
611
|
+
),
|
|
612
|
+
);
|
|
554
613
|
}
|
|
555
614
|
}
|
|
556
615
|
}
|
|
@@ -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
|
});
|
|
@@ -38,3 +38,9 @@ From the repo root, non-interactive runs should load the SRAI MCP server and all
|
|
|
38
38
|
`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
39
|
|
|
40
40
|
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.
|
|
41
|
+
|
|
42
|
+
## Codex CLI (scripted)
|
|
43
|
+
|
|
44
|
+
From the repo root, non-interactive runs should execute via `codex exec` and include the SRAI MCP server configuration for that run.
|
|
45
|
+
|
|
46
|
+
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,14 +72,14 @@ 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
81
|
vscode: '.github/skills/threat-modelling',
|
|
82
|
-
codex: '.
|
|
82
|
+
codex: '.codex/skills/threat-modelling',
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
/** Relative workspace paths for the CTM sync agent/workflow (per IDE / CLI). */
|
|
@@ -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,55 @@ 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 Codex login in the current terminal. Device auth keeps the flow in-terminal.
|
|
114
|
+
*/
|
|
115
|
+
export function runCodexLogin(cwd) {
|
|
116
|
+
const env = augmentPathEnv(process.env);
|
|
117
|
+
if (!commandOk('codex', ['--version'], env)) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
status: null,
|
|
121
|
+
message: 'Codex CLI not found (`codex`). Install with `npm install -g @openai/codex`.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const r = spawnSync('codex', ['login', '--device-auth'], { 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
|
+
function escapeTomlString(value) {
|
|
133
|
+
return String(value || '').replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readCodexMcpEnv(cwd) {
|
|
137
|
+
const config = readText(join(cwd, '.codex', 'config.toml'));
|
|
138
|
+
const apiUrl = config.match(/^\s*SECURITY_REVIEW_API_URL\s*=\s*"([^"]*)"/m)?.[1] || '';
|
|
139
|
+
const apiToken = config.match(/^\s*SECURITY_REVIEW_API_TOKEN\s*=\s*"([^"]*)"/m)?.[1] || '';
|
|
140
|
+
return { apiUrl, apiToken };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function buildCodexConfigOverrides(cwd, overrides = {}) {
|
|
144
|
+
const fileValues = readCodexMcpEnv(cwd);
|
|
145
|
+
const apiUrl = String(overrides.apiUrl || fileValues.apiUrl || '').trim();
|
|
146
|
+
const apiToken = String(overrides.apiToken || fileValues.apiToken || '').trim();
|
|
147
|
+
|
|
148
|
+
if (!apiUrl || !apiToken) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
'features.codex_hooks=true',
|
|
154
|
+
`mcp_servers.${MCP_SERVER_NAME}.command="npx"`,
|
|
155
|
+
`mcp_servers.${MCP_SERVER_NAME}.args=["-y","${MCP_SERVER_PACKAGE}@latest"]`,
|
|
156
|
+
`mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_URL="${escapeTomlString(apiUrl)}"`,
|
|
157
|
+
`mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_TOKEN="${escapeTomlString(apiToken)}"`,
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
75
161
|
export function pickProfilerAgentTarget(targets) {
|
|
76
162
|
for (const t of PREFERRED_ORDER) {
|
|
77
163
|
if (targets.includes(t)) {
|
|
@@ -94,10 +180,18 @@ export function pickProfilerAgentTarget(targets) {
|
|
|
94
180
|
*/
|
|
95
181
|
export function runProfilerAgent(
|
|
96
182
|
cwd,
|
|
97
|
-
{
|
|
183
|
+
{
|
|
184
|
+
target,
|
|
185
|
+
projectName,
|
|
186
|
+
apiUrl,
|
|
187
|
+
apiToken,
|
|
188
|
+
cursorTrust = true,
|
|
189
|
+
streamProgress = false,
|
|
190
|
+
showOutput = streamProgress,
|
|
191
|
+
},
|
|
98
192
|
) {
|
|
99
193
|
const prompt = buildProfilerAgentPrompt(projectName, target);
|
|
100
|
-
const env =
|
|
194
|
+
const env = buildProfilerEnv(target, streamProgress);
|
|
101
195
|
const opts = showOutput
|
|
102
196
|
? { cwd, stdio: 'inherit', env }
|
|
103
197
|
: { cwd, stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 };
|
|
@@ -129,7 +223,7 @@ export function runProfilerAgent(
|
|
|
129
223
|
if (r.error) {
|
|
130
224
|
return { ok: false, message: r.error.message };
|
|
131
225
|
}
|
|
132
|
-
return buildProfilerResult(r);
|
|
226
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
133
227
|
}
|
|
134
228
|
|
|
135
229
|
if (target === 'claude') {
|
|
@@ -140,16 +234,31 @@ export function runProfilerAgent(
|
|
|
140
234
|
? ['-p', '--output-format', 'stream-json', '--include-partial-messages', '--verbose', prompt]
|
|
141
235
|
: ['-p', prompt];
|
|
142
236
|
const r = spawnSync('claude', args, opts);
|
|
143
|
-
return buildProfilerResult(r);
|
|
237
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
144
238
|
}
|
|
145
239
|
|
|
146
240
|
if (target === 'codex') {
|
|
147
241
|
if (!commandOk('codex', ['--version'], env)) {
|
|
148
242
|
return { ok: false, message: 'codex not on PATH' };
|
|
149
243
|
}
|
|
150
|
-
const
|
|
244
|
+
const configOverrides = buildCodexConfigOverrides(cwd, { apiUrl, apiToken });
|
|
245
|
+
if (!configOverrides) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
message:
|
|
249
|
+
'Codex profiling needs SRAI MCP credentials. Re-run init with Codex selected and MCP installation enabled.',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const args = ['exec', '--full-auto'];
|
|
253
|
+
for (const override of configOverrides) {
|
|
254
|
+
args.push('-c', override);
|
|
255
|
+
}
|
|
256
|
+
if (streamProgress) {
|
|
257
|
+
args.push('--json');
|
|
258
|
+
}
|
|
259
|
+
args.push(prompt);
|
|
151
260
|
const r = spawnSync('codex', args, opts);
|
|
152
|
-
return buildProfilerResult(r);
|
|
261
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
153
262
|
}
|
|
154
263
|
|
|
155
264
|
if (target === 'vscode') {
|
|
@@ -177,7 +286,7 @@ export function runProfilerAgent(
|
|
|
177
286
|
args.push('--output-format', 'json');
|
|
178
287
|
}
|
|
179
288
|
const r = spawnSync('copilot', args, opts);
|
|
180
|
-
return buildProfilerResult(r);
|
|
289
|
+
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
181
290
|
}
|
|
182
291
|
|
|
183
292
|
return { ok: false, message: 'unsupported agent target' };
|
|
@@ -207,13 +316,13 @@ export function buildCopilotAdditionalMcpConfig(cwd) {
|
|
|
207
316
|
});
|
|
208
317
|
}
|
|
209
318
|
|
|
210
|
-
function buildProfilerResult(result) {
|
|
319
|
+
function buildProfilerResult(result, extras = {}) {
|
|
211
320
|
if (result.error) {
|
|
212
|
-
return { ok: false, status: result.status, message: result.error.message };
|
|
321
|
+
return { ok: false, status: result.status, message: result.error.message, ...extras };
|
|
213
322
|
}
|
|
214
323
|
|
|
215
324
|
if (result.status === 0) {
|
|
216
|
-
return { ok: true, status: result.status };
|
|
325
|
+
return { ok: true, status: result.status, ...extras };
|
|
217
326
|
}
|
|
218
327
|
|
|
219
328
|
const outputTail = summarizeProfilerOutput(result.stderr || result.stdout || '');
|
|
@@ -228,6 +337,7 @@ function buildProfilerResult(result) {
|
|
|
228
337
|
ok: false,
|
|
229
338
|
status: result.status,
|
|
230
339
|
message: outputTail ? `${exitDetail}; last output: ${outputTail}` : exitDetail,
|
|
340
|
+
...extras,
|
|
231
341
|
};
|
|
232
342
|
}
|
|
233
343
|
|
|
@@ -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
|
+
});
|