Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 29 additions & 22 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,14 +663,18 @@ export class CodexSecurity {
`Shell-visible plugin root must be outside CODEX_HOME: ${canonicalShellPluginRoot}`,
);
}
const basePrompt = await scanPrompt(
shellPluginRoot,
normalized,
mode,
runtime.configPath !== undefined,
knowledgeBase !== null,
options.scanPrompt,
);
const skillName = skillNameFor(normalized, mode);
const skillPath = join(shellPluginRoot, "skills", skillName, "SKILL.md");
const skillMetadata = await lstat(skillPath).catch(() => null);
if (
skillMetadata === null ||
!skillMetadata.isFile() ||
skillMetadata.isSymbolicLink()
) {
throw new IncompleteScanError(
`Installed plugin is missing scan skill: ${skillName}`,
);
}
checkOpen();
const expectation: ScanExpectation = {
repository: repo,
Expand Down Expand Up @@ -866,6 +870,16 @@ export class CodexSecurity {
}
activeScan = { id: scanId, options: workbenchOptions };
checkOpen();
const basePrompt = scanPrompt(
normalized,
mode,
skillName,
scanId,
runtime.configPath !== undefined,
knowledgeBase !== null,
options.scanPrompt,
);
checkOpen();
const feedback = await workbench(
{
...workbenchOptions,
Expand Down Expand Up @@ -2117,32 +2131,25 @@ function trustedAccessWarning(
return `Some cybersecurity requests or findings may be refused because ${access} could not be verified. Check ${action} or apply at ${applicationUrl}.`;
}

async function scanPrompt(
pluginRoot: string,
function scanPrompt(
target: NormalizedTarget,
mode: ScanMode,
skillName: string,
scanId: string,
hasConfigPath = false,
hasKnowledgeBase = false,
additionalPrompt?: string,
): Promise<string> {
const skillName = skillNameFor(target, mode);
const skillPath = join(pluginRoot, "skills", skillName, "SKILL.md");
const metadata = await lstat(skillPath).catch(() => null);
if (metadata === null || !metadata.isFile() || metadata.isSymbolicLink()) {
throw new IncompleteScanError(
`Installed plugin is missing scan skill: ${skillName}`,
);
}
): string {
return [
`Use the installed $codex-security:${skillName} skill at "$CODEX_SECURITY_PLUGIN_ROOT/skills/${skillName}/SKILL.md".`,
"Run this Codex Security scan non-interactively.",
...(mode === "deep"
? [
'The SDK has already registered this scan. Call start_codex_security_deep_scan with { scanId: "$CODEX_SECURITY_SCAN_ID" }; never pass targetPath or create another scan.',
`The SDK has already registered this scan. Call start_codex_security_deep_scan with ${JSON.stringify({ scanId })}; never pass targetPath or create another scan.`,
]
: skillName === "security-scan"
? [
'The SDK has already registered this scan. Use exactly "$CODEX_SECURITY_SCAN_ID" and "$CODEX_SECURITY_SCAN_DIR"; never call a scan-start or completion tool, and leave finalization to the SDK.',
`The SDK has already registered this scan. Use exactly ${JSON.stringify(scanId)} and "$CODEX_SECURITY_SCAN_DIR"; never call a scan-start or completion tool, and leave finalization to the SDK.`,
]
: []),
...(skillName === "security-scan"
Expand All @@ -2158,7 +2165,7 @@ async function scanPrompt(
'Use "$PYTHON" as <python_command> for every plugin helper; replace any literal python or python3 helper invocation with this exact interpreter.',
'Repository root: "$CODEX_SECURITY_REPOSITORY"',
'Use this exact scan directory for all scan output: "$CODEX_SECURITY_SCAN_DIR"',
'Use exactly "$CODEX_SECURITY_SCAN_ID" as the scan ID in the manifest, findings, and coverage.',
`Use exactly ${JSON.stringify(scanId)} as the scan ID in the manifest, findings, and coverage.`,
'Use exactly "$CODEX_SECURITY_TARGET_ID" as scan.target.targetId; do not derive a different target ID.',
'Use exactly "$CODEX_SECURITY_TARGET_DISPLAY_NAME" as scan.target.displayName; do not infer a display name from the Git remote.',
'Use exactly "$CODEX_SECURITY_TARGET_KIND" as scan.target.kind; do not infer the target kind from the checkout.',
Expand Down
121 changes: 120 additions & 1 deletion sdk/typescript/tests-ts/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2656,6 +2656,125 @@ describe("CodexSecurity orchestration", () => {
await client.close();
});

test("rejects a missing scan skill before registering a scan", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const codexHome = join(root, "codex-home");
const pluginRoot = join(root, "plugin-without-skills");
const scanDir = join(root, "scan");
await mkdir(repository);
await mkdir(codexHome);
await mkdir(pluginRoot);
await mkdir(scanDir, { mode: 0o700 });
const runtime = preparedRuntime(codexHome);
const commands: string[] = [];
const client = new TestClient(
{},
{
environment: {},
prepareRuntime: async () => ({
...runtime,
plugin: {
...(runtime["plugin"] as Record<string, unknown>),
pluginRoot,
marketplaceRoot: pluginRoot,
installedRoot: pluginRoot,
},
}),
resolvePluginPython: async () => "/managed/python",
prepareOutputDir: async () => scanDir,
repositoryRevision: async () => "deadbeef",
runWorkbench: async (_options: unknown, args: readonly string[]) => {
commands.push(args[0]!);
return args[0] === "register-cli-scan"
? mockScanRegistration(args)
: {};
},
},
);

await expect(client.run(repository)).rejects.toThrow(
"Installed plugin is missing scan skill: security-scan",
);
expect(commands).toEqual([]);
await client.close();
});

test.each([
["standard without feedback", "standard", false],
["standard with feedback", "standard", true],
["deep without feedback", "deep", false],
["deep with feedback", "deep", true],
] as const)(
"uses the registered scan ID in %s",
async (_scenario, mode, withFeedback) => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const codexHome = join(root, "codex-home");
const scanDir = join(root, "scan");
const scanId = "123e4567-e89b-12d3-a456-426614174000";
await mkdir(repository);
await mkdir(codexHome);
await mkdir(scanDir, { mode: 0o700 });
let prompt = "";
const client = new TestClient(
{},
{
environment: {},
prepareRuntime: async () => preparedRuntime(codexHome),
resolvePluginPython: async () => "/managed/python",
prepareOutputDir: async () => scanDir,
repositoryRevision: async () => "deadbeef",
runWorkbench: async (_options: unknown, args: readonly string[]) => {
if (args[0] === "register-cli-scan") {
return { ...mockScanRegistration(args), scanId };
}
if (args[0] === "get-scan-feedback") {
return {
scanId,
targetId: "target_sha256_example",
falsePositives: withFeedback
? [{ reason: "The finding is no longer reproducible." }]
: [],
};
}
return {};
},
createCodex: () => ({
startThread: () => ({
id: null,
async runStreamed(input: string) {
prompt = input;
throw new Error("prompt captured");
},
}),
}),
},
);

await expect(client.run(repository, { mode })).rejects.toThrow(
"prompt captured",
);
expect(prompt).toContain(
`Use exactly "${scanId}" as the scan ID in the manifest, findings, and coverage.`,
);
expect(prompt).not.toContain("$CODEX_SECURITY_SCAN_ID");
if (mode === "deep") {
const deepScanArguments = prompt.match(
/start_codex_security_deep_scan with (\{[^\n]+\});/,
);
expect(deepScanArguments).not.toBeNull();
expect(JSON.parse(deepScanArguments![1]!)).toEqual({ scanId });
} else {
expect(prompt).not.toContain("start_codex_security_deep_scan");
}
expect(prompt.includes("false_positive_feedback.json")).toBe(
withFeedback,
);
await client.close();
},
);

test.each([
["semantic matching fails", "matcher", "matcher unavailable"],
["the repository index fails", "index", "index unavailable"],
Expand Down Expand Up @@ -4676,7 +4795,7 @@ describe("CodexSecurity orchestration", () => {
);
expect(prompt).toContain("$codex-security:deep-security-scan");
expect(prompt).toContain(
'start_codex_security_deep_scan with { scanId: "$CODEX_SECURITY_SCAN_ID" }',
'start_codex_security_deep_scan with {"scanId":"scan_example_001"}',
);
expect(prompt).not.toContain(
"This exhaustive scan authorizes the delegated-worker phases",
Expand Down
Loading