@securityreviewai/securityreview-kit 0.1.36 → 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` |
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.36",
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
  }
@@ -2,6 +2,29 @@ import { join } from 'node:path';
2
2
  import { readText, writeText, ensureDir } from '../../utils/fs-helpers.js';
3
3
  import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
4
4
 
5
+ function ensureHooksFeature(existing) {
6
+ const featureLine = 'codex_hooks = true';
7
+ const featuresHeader = '[features]';
8
+
9
+ if (!existing) {
10
+ return `${featuresHeader}\n${featureLine}`;
11
+ }
12
+
13
+ if (existing.includes(featuresHeader)) {
14
+ const featuresSectionPattern = /(\[features\][\s\S]*?)(?=\n\[[^\]]+\]|$)/;
15
+ return existing.replace(featuresSectionPattern, (section) => {
16
+ if (/\bcodex_hooks\s*=/.test(section)) {
17
+ return section.replace(/codex_hooks\s*=\s*(true|false)/, featureLine);
18
+ }
19
+ const separator = section.endsWith('\n') ? '' : '\n';
20
+ return `${section}${separator}${featureLine}\n`;
21
+ });
22
+ }
23
+
24
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
25
+ return `${existing}${separator}${featuresHeader}\n${featureLine}\n`;
26
+ }
27
+
5
28
  /**
6
29
  * Generate Codex MCP config at .codex/config.toml
7
30
  * Codex uses TOML format. We use simple string templating since the structure
@@ -21,23 +44,25 @@ SECURITY_REVIEW_API_URL = "${envVars.apiUrl}"
21
44
  SECURITY_REVIEW_API_TOKEN = "${envVars.apiToken}"
22
45
  `.trim();
23
46
 
47
+ const existingWithHooks = ensureHooksFeature(existing).trim();
48
+
24
49
  // Check if we already have this server configured
25
- if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
50
+ if (existingWithHooks.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
26
51
  // Replace the existing block — find from the server header to the next
27
52
  // section header or end of file
28
53
  const regex = new RegExp(
29
54
  `\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[(?!mcp_servers\\.${MCP_SERVER_NAME})|$)`,
30
55
  );
31
- const updated = existing.replace(regex, serverBlock);
56
+ const updated = existingWithHooks.replace(regex, serverBlock);
32
57
  writeText(filePath, updated);
33
- } else if (existing) {
58
+ } else if (existingWithHooks) {
34
59
  // Append to existing file
35
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
36
- writeText(filePath, existing + separator + serverBlock + '\n');
60
+ const separator = existingWithHooks.endsWith('\n') ? '\n' : '\n\n';
61
+ writeText(filePath, existingWithHooks + separator + serverBlock + '\n');
37
62
  } else {
38
63
  // New file
39
64
  ensureDir(join(cwd, '.codex'));
40
- writeText(filePath, serverBlock + '\n');
65
+ writeText(filePath, existingWithHooks + '\n\n' + serverBlock + '\n');
41
66
  }
42
67
 
43
68
  return filePath;
@@ -0,0 +1,42 @@
1
+ import { mkdtempSync, mkdirSync, 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 './codex.js';
7
+
8
+ test('Codex MCP generator enables hooks and writes MCP server block', () => {
9
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-mcp-'));
10
+
11
+ generate(cwd, {
12
+ apiUrl: 'https://example.test',
13
+ apiToken: 'secret-token',
14
+ });
15
+
16
+ const config = readFileSync(join(cwd, '.codex/config.toml'), 'utf8');
17
+ assert.match(config, /\[features\]\ncodex_hooks = true/);
18
+ assert.match(config, /\[mcp_servers\.security-review-mcp\]/);
19
+ assert.match(config, /SECURITY_REVIEW_API_URL = "https:\/\/example\.test"/);
20
+ });
21
+
22
+ test('Codex MCP generator preserves existing features while forcing codex_hooks', () => {
23
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-mcp-merge-'));
24
+ const configPath = join(cwd, '.codex', 'config.toml');
25
+ mkdirSync(join(cwd, '.codex'), { recursive: true });
26
+
27
+ writeFileSync(
28
+ configPath,
29
+ '[features]\nshell_snapshot = true\ncodex_hooks = false\n\n[profiles.default]\nmodel = "gpt-5.4"\n',
30
+ 'utf8',
31
+ );
32
+
33
+ generate(cwd, {
34
+ apiUrl: 'https://example.test',
35
+ apiToken: 'secret-token',
36
+ });
37
+
38
+ const config = readFileSync(configPath, 'utf8');
39
+ assert.match(config, /\[features\][\s\S]*shell_snapshot = true/);
40
+ assert.match(config, /\[features\][\s\S]*codex_hooks = true/);
41
+ assert.match(config, /\[profiles\.default\]\nmodel = "gpt-5\.4"/);
42
+ });
@@ -1,28 +1,146 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { join } from 'node:path';
2
- import { GUARDRAILS_PROFILER_SKILL_REL_DIR, GUARDRAILS_SELECTION_SKILL_REL_DIR } from '../../utils/constants.js';
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 { getRuleContent, getGuardrailsInitProfileContent } from './content.js';
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 getCodexSessionHookContent() {
24
+ const additionalContext = [
25
+ '## MANDATORY SECURITY GATE (Session Policy)',
26
+ '',
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
+ '',
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
+ '3. Implement secure code using both the hydrated guardrails and PWNISMS findings.',
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
+ '',
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
+ ].join('\n');
36
+
37
+ const sessionStartPayload = JSON.stringify({
38
+ hookSpecificOutput: {
39
+ hookEventName: 'SessionStart',
40
+ additionalContext,
41
+ },
42
+ });
43
+ const userPromptPayload = JSON.stringify({
44
+ hookSpecificOutput: {
45
+ hookEventName: 'UserPromptSubmit',
46
+ additionalContext,
47
+ },
48
+ });
49
+
50
+ return JSON.stringify(
51
+ {
52
+ hooks: {
53
+ SessionStart: [
54
+ {
55
+ matcher: 'startup|resume',
56
+ hooks: [
57
+ {
58
+ type: 'command',
59
+ command: `printf '%s\\n' '${sessionStartPayload.replaceAll("'", "'\\''")}'`,
60
+ timeout: 5,
61
+ statusMessage: 'Loading SRAI security session policy',
62
+ },
63
+ ],
64
+ },
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
+ ],
78
+ },
79
+ },
80
+ null,
81
+ 2,
82
+ ) + '\n';
83
+ }
84
+
85
+ function getAgentBody(content) {
86
+ return content.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
87
+ }
88
+
89
+ function getCodexCtmSyncAgentContent(options = {}) {
90
+ const developerInstructions = getAgentBody(getCtmSyncWorkflowContent(options));
91
+ return [
92
+ 'name = "ctm_sync"',
93
+ 'description = "Synchronize threat-model and guardrail data to SRAI after security-relevant work."',
94
+ "developer_instructions = '''",
95
+ developerInstructions,
96
+ "'''",
97
+ '',
98
+ ].join('\n');
99
+ }
5
100
 
6
101
  /**
7
102
  * Generate Codex workspace rule — appends to AGENTS.md
8
103
  */
9
104
  export function generate(cwd, options = {}) {
10
- const filePath = join(cwd, 'AGENTS.md');
11
- const content = getRuleContent({
105
+ const optionsWithSkillDirs = {
12
106
  ...options,
13
107
  guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.codex,
14
- });
108
+ threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.codex,
109
+ ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.codex,
110
+ };
111
+ const filePath = join(cwd, '.codex', 'AGENTS.md');
112
+ const content = getRuleContent(optionsWithSkillDirs);
15
113
  const action = upsertSentinelBlock(filePath, content);
16
114
 
115
+ const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.codex, 'SKILL.md');
116
+ const threatSkill = writeGeneratedText(
117
+ threatSkillPath,
118
+ getThreatModellingSkillContent(optionsWithSkillDirs),
119
+ );
120
+
121
+ const ctmSyncAgentPath = join(cwd, CTM_SYNC_AGENT_REL_PATH.codex);
122
+ const ctmSyncAgent = writeGeneratedText(
123
+ ctmSyncAgentPath,
124
+ getCodexCtmSyncAgentContent(optionsWithSkillDirs),
125
+ );
126
+
127
+ const hooksPath = join(cwd, '.codex', 'hooks.json');
128
+ const hooks = writeGeneratedText(hooksPath, getCodexSessionHookContent());
129
+
17
130
  const guardrailsInitPath = join(cwd, '.codex', 'commands', 'guardrails-init-profile.md');
18
- const guardrailsInitContent = getGuardrailsInitProfileContent({
19
- ...options,
20
- guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.codex,
21
- });
22
- writeText(guardrailsInitPath, guardrailsInitContent);
131
+ const guardrailsInit = writeGeneratedText(
132
+ guardrailsInitPath,
133
+ getGuardrailsInitProfileContent({
134
+ ...optionsWithSkillDirs,
135
+ guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.codex,
136
+ }),
137
+ );
23
138
 
24
139
  return [
25
140
  { filePath, action, kind: 'rule' },
26
- { filePath: guardrailsInitPath, action: 'created', kind: 'command' },
141
+ { ...threatSkill, kind: 'skill' },
142
+ { ...ctmSyncAgent, kind: 'agent' },
143
+ { ...hooks, kind: 'hooks' },
144
+ { ...guardrailsInit, kind: 'command' },
27
145
  ];
28
146
  }
@@ -0,0 +1,50 @@
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 './codex.js';
7
+
8
+ test('Codex generator writes AGENTS, skills, subagent, hooks, and profiling command', () => {
9
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-'));
10
+
11
+ const results = generate(cwd, { projectName: 'SmokeProject' });
12
+
13
+ const expectedPaths = [
14
+ '.codex/AGENTS.md',
15
+ '.codex/skills/threat-modelling/SKILL.md',
16
+ '.codex/agents/ctm_sync.toml',
17
+ '.codex/hooks.json',
18
+ '.codex/commands/guardrails-init-profile.md',
19
+ ];
20
+
21
+ for (const relPath of expectedPaths) {
22
+ assert.equal(existsSync(join(cwd, relPath)), true, `${relPath} should exist`);
23
+ }
24
+
25
+ assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'agent', 'hooks', 'command']);
26
+
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
+ assert.match(instructions, /\.codex\/agents\/ctm_sync\.toml/);
31
+ assert.doesNotMatch(instructions, /\.cursor/);
32
+ assert.doesNotMatch(instructions, /\.agents\/skills/);
33
+
34
+ const threatSkill = readFileSync(join(cwd, '.codex/skills/threat-modelling/SKILL.md'), 'utf8');
35
+ for (const heading of ['Product', 'Workload', 'Network', 'IAM', 'Secrets', 'Monitoring', 'Supply Chain']) {
36
+ assert.match(threatSkill, new RegExp(heading));
37
+ }
38
+ assert.match(threatSkill, /\.codex\/agents\/ctm_sync\.toml/);
39
+ assert.doesNotMatch(threatSkill, /get_project_profile_description/);
40
+
41
+ const agent = readFileSync(join(cwd, '.codex/agents/ctm_sync.toml'), 'utf8');
42
+ assert.match(agent, /name = "ctm_sync"/);
43
+ assert.match(agent, /developer_instructions = '''/);
44
+ assert.match(agent, /Configured SRAI project name: `SmokeProject`/);
45
+
46
+ const hooks = JSON.parse(readFileSync(join(cwd, '.codex/hooks.json'), 'utf8'));
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"/);
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 `.codex/skills/guardrails-profiler` 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
  },
@@ -79,12 +79,14 @@ export const GUARDRAILS_SELECTION_SKILL_REL_DIR = {
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: '.codex/skills/threat-modelling',
82
83
  };
83
84
 
84
85
  /** Relative workspace paths for the CTM sync agent/workflow (per IDE / CLI). */
85
86
  export const CTM_SYNC_AGENT_REL_PATH = {
86
87
  cursor: '.cursor/agents/ctm_sync.md',
87
88
  vscode: '.github/agents/ctm_sync.agent.md',
89
+ codex: '.codex/agents/ctm_sync.toml',
88
90
  };
89
91
 
90
92
  export const SENTINEL_START = '<!-- securityreview-kit:start -->';
@@ -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
+ });