@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 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` | `AGENTS.md`, `.agents/skills/threat-modelling/SKILL.md`, `.agents/skills/guardrails-profiler/SKILL.md`, `.agents/skills/guardrails-selection/SKILL.md`, `.codex/agents/ctm_sync.toml`, `.codex/hooks.json`, `.codex/commands/guardrails-init-profile.md` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securityreviewai/securityreview-kit",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Bootstrap security-review-mcp for AI IDEs and CLI tools",
5
5
  "author": "Debarshi Das <debarshi.das@we45.com>",
6
6
  "license": "UNLICENSED",
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) {
@@ -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
- options.profilerVerbose || (agentTarget === 'cursor' && options.profilerNoTrust),
526
+ profilerVerbose || (agentTarget === 'cursor' && options.profilerNoTrust),
494
527
  );
495
528
  if (showProfilerOutput) {
496
- console.log(chalk.dim(' Profiling in progress. Agent output is visible for this run...'));
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: Boolean(options.profilerVerbose),
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 .agents/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 .agents/skills/threat-modelling/SKILL.md. Explicitly walk Product, Workload, Network, IAM, Secrets, Monitoring, and Supply Chain.',
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 commandPayload = JSON.stringify({
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' '${commandPayload.replaceAll("'", "'\\''")}'`,
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
- '.agents/skills/threat-modelling/SKILL.md',
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, /\.agents\/skills\/guardrails-selection\/SKILL\.md/);
29
- assert.match(instructions, /\.agents\/skills\/threat-modelling\/SKILL\.md/);
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, '.agents/skills/threat-modelling/SKILL.md'), 'utf8');
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 `.agents/skills/guardrails-profiler` for Codex depending on where this file was installed.
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
@@ -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: '.agents/skills/guardrails-profiler',
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: '.agents/skills/guardrails-selection',
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: '.agents/skills/threat-modelling',
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
- { target, projectName, cursorTrust = true, streamProgress = false, showOutput = streamProgress },
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 = augmentPathEnv(process.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 args = streamProgress ? ['exec', '--json', prompt] : ['exec', prompt];
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
+ });