/** * pi-codex-security — Pi extension for OpenAI Codex Security. * * Registers: * - security_scan tool: run a scan, stream progress, return findings summary * - security_auth_status tool: check Codex Security authentication * - /security-scan command: ask the agent to scan (findings land in context) * - /security-status command: show auth status */ import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { formatCost, formatScanReport, SEVERITY_LEVELS, type Severity, } from "./format.ts"; import { checkAuthStatus, runCodexSecurityScan } from "./scan.ts"; export default function (pi: ExtensionAPI) { pi.registerTool({ name: "security_scan", label: "Codex Security Scan", description: "Run an OpenAI Codex Security scan on a repository to find, validate, and get remediation guidance for security vulnerabilities. " + "Returns a severity-ranked findings summary plus paths to the full report (report.md), findings.json, and SARIF export. " + "Requires Codex Security authentication (check with security_auth_status). Scans can take several minutes.", promptSnippet: "Scan the repo for security vulnerabilities with Codex Security", promptGuidelines: [ "Use security_scan when the user asks to audit, scan, or review code for security vulnerabilities.", "Use security_scan with diffBase (e.g. main) to review only uncommitted or branch changes instead of the whole repository.", "After fixing findings from security_scan, re-run security_scan on the same target to verify the fixes.", ], parameters: Type.Object({ path: Type.Optional( Type.String({ description: "Repository or directory to scan. Defaults to the current working directory.", }), ), mode: Type.Optional( StringEnum(["standard", "deep"], { description: 'Scan mode: "standard" (default) or "deep" (more thorough, slower, costs more).', }), ), auth: Type.Optional( StringEnum(["auto", "chatgpt", "api-key"], { description: 'Credential to use: "auto" (default), "chatgpt" (browser sign-in), or "api-key" (OPENAI_API_KEY/CODEX_API_KEY).', }), ), diffBase: Type.Optional( Type.String({ description: "Only scan changes relative to this git base ref (working-tree diff). Combine with diffHead to scan a ref range instead.", }), ), diffHead: Type.Optional( Type.String({ description: "Head ref for a ref-range diff scan (requires diffBase). Example: diffBase=main, diffHead=HEAD.", }), ), minSeverity: Type.Optional( StringEnum(SEVERITY_LEVELS, { description: 'Minimum severity to list individually in the result: "critical", "high", "medium", "low" (default), or "informational". Counts always include all findings.', }), ), maxCostUsd: Type.Optional( Type.Number({ description: "Abort the scan if estimated cost exceeds this many USD.", }), ), outputDir: Type.Optional( Type.String({ description: "Directory for scan output. Defaults to the Codex Security state directory.", }), ), }), async execute(_toolCallId, params, signal, onUpdate, ctx) { const targetPath = params.path ?? ctx.cwd; const minSeverity = (params.minSeverity ?? "low") as Severity; const mode = params.mode as "standard" | "deep" | undefined; const auth = params.auth as "auto" | "chatgpt" | "api-key" | undefined; try { const outcome = await runCodexSecurityScan( { path: targetPath, ...(mode !== undefined ? { mode } : {}), ...(auth !== undefined ? { auth } : {}), ...(params.diffBase !== undefined ? { diffBase: params.diffBase } : {}), ...(params.diffHead !== undefined ? { diffHead: params.diffHead } : {}), ...(params.maxCostUsd !== undefined ? { maxCostUsd: params.maxCostUsd } : {}), ...(params.outputDir !== undefined ? { outputDir: params.outputDir } : {}), }, (message) => onUpdate?.({ content: [{ type: "text", text: message }], details: {}, }), signal, ); const costText = outcome.cost ? formatCost(outcome.cost) : undefined; const text = formatScanReport({ findings: outcome.findings, minSeverity, reportPath: outcome.reportPath, findingsPath: outcome.findingsPath, sarifPath: outcome.sarifPath, scanDir: outcome.scanDir, ...(costText !== undefined ? { costText } : {}), }); return { content: [{ type: "text", text }], details: { scanDir: outcome.scanDir, reportPath: outcome.reportPath, findingsPath: outcome.findingsPath, coveragePath: outcome.coveragePath, sarifPath: outcome.sarifPath, findingCount: outcome.findings.length, cost: outcome.cost, }, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Codex Security scan failed: ${message}`, }, ], isError: true, details: {}, }; } }, }); pi.registerTool({ name: "security_auth_status", label: "Codex Security Auth Status", description: "Check whether Codex Security is authenticated (ChatGPT sign-in or OPENAI_API_KEY/CODEX_API_KEY). " + "Call this before security_scan if a scan fails with an authentication error, or when the user asks about their Codex Security login.", promptSnippet: "Check Codex Security authentication status", parameters: Type.Object({}), async execute() { try { const status = await checkAuthStatus(); const lines = [ status.authenticated ? "Codex Security: authenticated." : "Codex Security: NOT authenticated.", `Details: ${status.details}`, ...status.envHints.map((hint) => `- ${hint}`), ]; if (!status.authenticated) { lines.push( "To sign in interactively: npx codex-security login", "For CI/non-interactive: set OPENAI_API_KEY.", ); } return { content: [{ type: "text", text: lines.join("\n") }], details: { authenticated: status.authenticated }, isError: !status.authenticated, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Could not check Codex Security status: ${message}`, }, ], isError: true, details: { authenticated: false }, }; } }, }); pi.registerCommand("security-scan", { description: "Ask the agent to run a Codex Security scan. Usage: /security-scan [path] [--deep] [--diff base[:head]] [--auth auto|chatgpt|api-key]", handler: async (args, ctx) => { const instruction = buildScanInstruction(args.trim(), ctx.cwd); pi.sendUserMessage(instruction); }, }); pi.registerCommand("security-status", { description: "Show Codex Security authentication status", handler: async (_args, ctx) => { try { const status = await checkAuthStatus(); const text = status.authenticated ? `Codex Security: authenticated — ${status.details}` : `Codex Security: not authenticated — ${status.details}. Run: npx codex-security login`; ctx.ui.notify(text, status.authenticated ? "info" : "warning"); } catch (error) { ctx.ui.notify( `Codex Security status check failed: ${error instanceof Error ? error.message : String(error)}`, "error", ); } }, }); } /** Turn /security-scan args into a precise instruction for the agent. */ export function buildScanInstruction(args: string, cwd: string): string { const tokens = args.split(/\s+/).filter(Boolean); let path: string | undefined; let deep = false; let diff: string | undefined; let auth: string | undefined; for (let i = 0; i < tokens.length; i += 1) { const token = tokens[i]; if (token === "--deep") { deep = true; } else if (token === "--diff") { diff = tokens[i + 1]; i += 1; } else if (token === "--auth") { auth = tokens[i + 1]; i += 1; } else if (token !== undefined && !token.startsWith("--")) { path = token; } } const parts = [ "Run a Codex Security scan with the security_scan tool", path !== undefined ? `on ${path}` : `on the current repository (${cwd})`, ]; if (deep) parts.push('with mode "deep"'); if (auth !== undefined) parts.push(`with auth "${auth}"`); if (diff !== undefined) { const [base, head] = diff.split(":"); parts.push( head !== undefined && head !== "" ? `scanning only changes between ${base} and ${head} (diffBase=${base}, diffHead=${head})` : `scanning only working-tree changes relative to ${base} (diffBase=${base})`, ); } parts.push( "When it finishes, summarize the findings by severity and offer to fix them, starting with the highest severity.", ); return `${parts.join(" ")}.`; }