feat(security): integrate Codex Security cloud scans

This commit is contained in:
Kyle McCleary
2026-07-29 23:24:26 -07:00
parent dba303e2e0
commit 0b968bbb49
12 changed files with 1279 additions and 51 deletions
+1
View File
@@ -7,6 +7,7 @@
- Added server-name autocomplete for `/mcp` commands (`enable`, `disable`, `test`, `remove`, `reconnect`, `reauth`, `unauth`) using configured and runtime-discovered MCP servers.
- Added `--from-claude` and `--from-codex` session imports, also available from `/resume @claude` and `/resume @codex`.
- Added an opt-in OMP-native software-security workflow (`security.enabled`, default off) with immutable scan plans, exact-account Codex subscription affinity, native task-worker review, canonical findings/coverage/SARIF publication, project-scoped history, explicit dispositions, producer-differential comparison, and the read-only `security://` resource namespace. Generic SARIF and official Codex Security bundles normalize into the same OMP-owned store.
- Added explicit Codex Security cloud operations to the opt-in security workflow: list and start account-pinned cloud scans, inspect their progress, and import current findings into OMP's canonical store and `security://` namespace without changing the native scan engine or spoofing official runtime attribution.
- Added `--from-claude` and `--from-codex` session imports (including compaction state for Codex), also available from `/resume @claude` and `/resume @codex`.
- Added interactive Exa API-key onboarding through `/login exa`, opening the official key dashboard and saving pasted keys for authenticated web search while preserving `EXA_API_KEY` and explicit-selection public MCP fallback behavior ([#1798](https://github.com/can1357/oh-my-pi/issues/1798)).
- Added `ExtensionContext.getAsyncJobSnapshot()` so extensions can read the owning session's async-job state without relying on process-global job-manager identity
@@ -1 +1 @@
Plan, start, inspect, or cancel an OMP-native repository security scan. `preflight` creates an immutable plan pinned to the repository snapshot, model, and exact OAuth credential. `start` runs the plan as a background OMP job. `status` and `cancel` operate on the returned operation ID. Security must be enabled in settings.
Plan, start, inspect, cancel, and validate OMP-native repository security scans. `preflight` creates an immutable plan pinned to the repository snapshot, model, and exact OAuth credential. `start` runs the plan as a background OMP job. `status` and `cancel` use the returned operation ID. `cloud_scans` lists Codex Security cloud configurations for the exact selected ChatGPT OAuth account. `cloud_start` creates and enables a cloud scan configuration using `repository_id`, `repository_url`, and `environment_id`; this consumes the account's separate Codex Security cloud allowance and is never a fallback from a native scan. `cloud_status` reads cloud progress. `cloud_pull` imports cloud findings into the canonical OMP security store, where they are available through `security://`. Cloud actions use `cloud_configuration_id` and may use `credential_id` to pin an account. Security must be enabled in settings.
+43 -9
View File
@@ -1,4 +1,5 @@
import type { AgentOptions } from "@oh-my-pi/pi-agent-core";
import type { OAuthAccessResolution } from "@oh-my-pi/pi-ai";
import type { ApiKeyResolver } from "@oh-my-pi/pi-ai/auth-retry";
import type { AuthStorage } from "../session/auth-storage";
import type { SecurityAccountRef } from "./contracts";
@@ -8,7 +9,7 @@ export interface ExactSecurityOAuthOptions {
account: SecurityAccountRef;
}
function assertIdentityMatches(
export function assertSecurityIdentityMatches(
account: SecurityAccountRef,
resolution: {
credentialId?: number;
@@ -29,6 +30,46 @@ function assertIdentityMatches(
}
}
export function selectSecurityAccount(
authStorage: AuthStorage,
provider: string,
requestedCredentialId?: number,
sessionId?: string,
): SecurityAccountRef {
const accounts = authStorage.listOAuthAccounts(provider, sessionId);
const selected =
requestedCredentialId !== undefined
? accounts.find(account => account.credentialId === requestedCredentialId)
: (accounts.find(account => account.active) ?? (accounts.length === 1 ? accounts[0] : undefined));
if (!selected) {
if (accounts.length === 0) throw new Error(`Security scans require a stored OAuth account for ${provider}`);
if (requestedCredentialId !== undefined) {
throw new Error(`Security OAuth credential ${requestedCredentialId} is not available for ${provider}`);
}
throw new Error(
`Multiple OAuth accounts are available for ${provider}; supply credentialId to pin one exact account`,
);
}
const account: SecurityAccountRef = { provider, credentialId: selected.credentialId };
if (selected.accountId !== undefined) account.accountId = selected.accountId;
if (selected.email !== undefined) account.email = selected.email;
if (selected.orgId !== undefined) account.organizationId = selected.orgId;
if (selected.orgName !== undefined) account.organizationName = selected.orgName;
return account;
}
export async function resolveExactSecurityOAuthAccess(
authStorage: AuthStorage,
account: SecurityAccountRef,
options: { forceRefresh: boolean; signal?: AbortSignal },
): Promise<Extract<OAuthAccessResolution, { ok: true }>> {
const resolution = await authStorage.getOAuthAccessByCredentialId(account.provider, account.credentialId, options);
if (!resolution) throw new Error("The pinned security OAuth credential is unavailable");
assertSecurityIdentityMatches(account, resolution);
if (!resolution.ok) throw new Error("The pinned security OAuth credential could not be resolved");
return resolution;
}
/**
* Build a request credential resolver pinned to one durable OAuth row.
*
@@ -46,17 +87,10 @@ export function createExactSecurityOAuthResolver(
}
const resolver: ApiKeyResolver = async context => {
if (context.lastChance) return undefined;
const resolution = await authStorage.getOAuthAccessByCredentialId(account.provider, account.credentialId, {
const resolution = await resolveExactSecurityOAuthAccess(authStorage, account, {
forceRefresh: context.error !== undefined,
signal: context.signal,
});
if (!resolution) {
throw new Error("The pinned security OAuth credential is unavailable");
}
assertIdentityMatches(account, resolution);
if (!resolution.ok) {
throw new Error("The pinned security OAuth credential could not be resolved");
}
return resolution.accessToken;
};
return resolver;
+687
View File
@@ -0,0 +1,687 @@
import { createHash } from "node:crypto";
import type { AuthStorage } from "../session/auth-storage";
import * as git from "../utils/git";
import { resolveExactSecurityOAuthAccess } from "./auth";
import {
createSecurityEvidenceId,
createSecurityFindingFingerprint,
createSecurityFindingId,
createSecurityOccurrenceId,
createSecurityScanId,
type SecurityAccountRef,
type SecurityConfidenceLevel,
type SecurityDispositionStatus,
type SecurityEvidence,
type SecurityFinding,
type SecurityLocation,
type SecurityProducer,
type SecurityProvenance,
type SecurityScanBundle,
type SecuritySeverityLevel,
} from "./contracts";
import { exportSecurityBundleToSarif } from "./sarif";
import type { SecurityStore } from "./store";
/**
* ChatGPT's Codex Security cloud control plane. This authenticated web-app
* contract is not a public OpenAI API: keep it isolated here, fail closed on
* shape changes, and never use it as a fallback for OMP-native inference.
*/
const DEFAULT_CLOUD_BASE_URL = "https://chatgpt.com/backend-api/aardvark";
const ALL_FINDING_STATUSES = ["new", "triaged", "in_progress", "fixed", "wontfix", "duplicate", "false_positive"];
const CLOUD_PRODUCER: SecurityProducer = {
kind: "codex-security-cloud",
name: "Codex Security cloud",
vendor: "OpenAI",
};
type JsonObject = Record<string, unknown>;
export type CodexSecurityCloudFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
export interface CodexSecurityCloudClientOptions {
authStorage: AuthStorage;
account: SecurityAccountRef;
baseUrl?: string;
fetch?: CodexSecurityCloudFetch;
}
export interface CodexSecurityCloudConfiguration {
id: string;
sourceId?: string;
repositoryId: string;
repositoryUrl: string;
environmentId: string;
state?: string;
currentStep?: string;
scanType?: string;
remainingScans?: number;
totalScans?: number;
createdAt?: string;
updatedAt?: string;
}
export interface CodexSecurityCloudConfigurationPage {
items: CodexSecurityCloudConfiguration[];
nextCursor?: string;
totalInAccount?: number;
}
export interface StartCodexSecurityCloudScanInput {
repositoryId: string;
repositoryUrl: string;
environmentId: string;
lookbackDays?: number | "all";
maintainerAttackConcerns?: string;
maintainerFocusAreas?: string;
maintainerAdditionalContext?: string;
signal?: AbortSignal;
}
export interface CodexSecurityCloudStats {
configurationId: string;
sourceConfigurationId?: string;
currentStep?: string;
pendingCommits: number;
finishedCommits: number;
failedCommits: number;
findingCounts: Record<SecuritySeverityLevel, number>;
lastScannedCommit?: string;
lastScannedAt?: string;
updatedAt?: string;
}
export interface PullCodexSecurityCloudResultsInput {
client: CodexSecurityCloudClient;
configurationId: string;
store: SecurityStore;
signal?: AbortSignal;
}
function object(value: unknown): JsonObject {
if (!value || typeof value !== "object" || Array.isArray(value))
throw new Error("Codex Security cloud returned an invalid object");
return value as JsonObject;
}
function optionalObject(value: unknown): JsonObject {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
}
function requiredString(value: unknown, field: string): string {
if (typeof value !== "string" || value.length === 0)
throw new Error(`Codex Security cloud response is missing ${field}`);
return value;
}
function optionalString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function finiteNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function positiveInteger(value: unknown): number | undefined {
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : undefined;
}
function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
function normalizeConfiguration(value: unknown): CodexSecurityCloudConfiguration {
const raw = object(value);
const scanInput = object(raw.scan_input);
const id = requiredString(raw.hid ?? raw.id, "configuration id");
const configuration: CodexSecurityCloudConfiguration = {
id,
repositoryId: requiredString(scanInput.repo_id, "repository id"),
repositoryUrl: requiredString(scanInput.repo_url, "repository URL"),
environmentId: requiredString(scanInput.environment_id, "environment id"),
};
const sourceId = optionalString(raw.id);
if (sourceId && sourceId !== id) configuration.sourceId = sourceId;
const state = optionalString(scanInput.state);
if (state) configuration.state = state;
const currentStep = optionalString(raw.current_step);
if (currentStep) configuration.currentStep = currentStep;
const scanType = optionalString(scanInput.scan_type);
if (scanType) configuration.scanType = scanType;
const remainingScans = typeof raw.scans_remaining === "number" ? raw.scans_remaining : raw.remaining_scans;
if (typeof remainingScans === "number" && Number.isFinite(remainingScans))
configuration.remainingScans = remainingScans;
if (typeof raw.total_scans === "number" && Number.isFinite(raw.total_scans))
configuration.totalScans = raw.total_scans;
const createdAt = optionalString(raw.created_at);
if (createdAt) configuration.createdAt = createdAt;
const updatedAt = optionalString(raw.updated_at);
if (updatedAt) configuration.updatedAt = updatedAt;
return configuration;
}
function jwtSubject(accessToken: string): string {
const payload = accessToken.split(".")[1];
if (!payload) throw new Error("The selected ChatGPT credential is not a JWT");
let claims: JsonObject;
try {
claims = object(JSON.parse(Buffer.from(payload, "base64url").toString("utf8")));
} catch {
throw new Error("The selected ChatGPT credential has an invalid JWT payload");
}
return requiredString(claims.sub ?? claims.user_id, "authenticated user id");
}
export class CodexSecurityCloudHttpError extends Error {
constructor(
readonly status: number,
readonly endpoint: string,
) {
super(`Codex Security cloud request failed (${status}) at ${endpoint}`);
this.name = "CodexSecurityCloudHttpError";
}
}
interface CloudRequestOptions {
method?: "GET" | "POST";
query?: Record<string, string | number | undefined>;
body?: JsonObject | ((accessToken: string) => JsonObject);
signal?: AbortSignal;
}
export class CodexSecurityCloudClient {
readonly #authStorage: AuthStorage;
readonly #account: SecurityAccountRef;
readonly #baseUrl: string;
readonly #fetch: CodexSecurityCloudFetch;
constructor(options: CodexSecurityCloudClientOptions) {
if (options.account.provider !== "openai-codex") {
throw new Error("Codex Security cloud requires an openai-codex ChatGPT OAuth credential");
}
this.#authStorage = options.authStorage;
this.#account = options.account;
this.#baseUrl = (options.baseUrl ?? DEFAULT_CLOUD_BASE_URL).replace(/\/$/, "");
this.#fetch = options.fetch ?? fetch;
}
async #request(pathname: string, options: CloudRequestOptions = {}): Promise<JsonObject> {
const url = new URL(`${this.#baseUrl}/${pathname.replace(/^\//, "")}`);
for (const [key, value] of Object.entries(options.query ?? {})) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
for (let attempt = 0; attempt < 2; attempt += 1) {
const access = await resolveExactSecurityOAuthAccess(this.#authStorage, this.#account, {
forceRefresh: attempt > 0,
signal: options.signal,
});
const body = typeof options.body === "function" ? options.body(access.accessToken) : options.body;
const headers: Record<string, string> = {
Accept: "application/json",
Authorization: `Bearer ${access.accessToken}`,
};
const accountId = access.accountId ?? this.#account.accountId;
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
if (body) headers["Content-Type"] = "application/json";
const response = await this.#fetch(url, {
method: options.method ?? "GET",
headers,
body: body ? JSON.stringify(body) : undefined,
signal: options.signal,
});
if (response.status === 401 && attempt === 0) continue;
if (!response.ok) throw new CodexSecurityCloudHttpError(response.status, url.pathname);
return object(await response.json());
}
throw new Error("Codex Security cloud authentication refresh failed");
}
async listConfigurations(
options: { limit?: number; cursor?: string; signal?: AbortSignal } = {},
): Promise<CodexSecurityCloudConfigurationPage> {
const raw = await this.#request("scan_configurations", {
query: { limit: options.limit ?? 100, cursor: options.cursor },
signal: options.signal,
});
const items = Array.isArray(raw.items) ? raw.items.map(normalizeConfiguration) : [];
const result: CodexSecurityCloudConfigurationPage = { items };
const nextCursor = optionalString(raw.next_cursor);
if (nextCursor) result.nextCursor = nextCursor;
if (typeof raw.total_in_account === "number") result.totalInAccount = raw.total_in_account;
return result;
}
async listAllConfigurations(signal?: AbortSignal): Promise<CodexSecurityCloudConfiguration[]> {
const configurations: CodexSecurityCloudConfiguration[] = [];
let cursor: string | undefined;
do {
const page = await this.listConfigurations({ limit: 500, cursor, signal });
configurations.push(...page.items);
cursor = page.nextCursor;
} while (cursor);
return configurations;
}
async getConfiguration(configurationId: string, signal?: AbortSignal): Promise<CodexSecurityCloudConfiguration> {
let cursor: string | undefined;
do {
const page = await this.listConfigurations({ limit: 500, cursor, signal });
const found = page.items.find(item => item.id === configurationId || item.sourceId === configurationId);
if (found) return found;
cursor = page.nextCursor;
} while (cursor);
throw new Error(`Unknown Codex Security cloud configuration: ${configurationId}`);
}
async startScan(input: StartCodexSecurityCloudScanInput): Promise<CodexSecurityCloudConfiguration> {
if (
input.lookbackDays !== undefined &&
input.lookbackDays !== "all" &&
(!Number.isInteger(input.lookbackDays) || input.lookbackDays < 1)
) {
throw new Error("lookbackDays must be a positive integer or 'all'");
}
const raw = await this.#request("scan_configurations", {
method: "POST",
signal: input.signal,
body: accessToken => {
const scanInput: JsonObject = {
environment_id: input.environmentId,
lookback_days: input.lookbackDays === "all" ? null : (input.lookbackDays ?? 30),
notification_rules: [],
owner_id: jwtSubject(accessToken),
repo_id: input.repositoryId,
repo_url: input.repositoryUrl,
share_targets: [],
state: "enabled",
};
if (input.maintainerAttackConcerns) scanInput.maintainer_attack_concerns = input.maintainerAttackConcerns;
if (input.maintainerFocusAreas) scanInput.maintainer_focus_areas = input.maintainerFocusAreas;
if (input.maintainerAdditionalContext)
scanInput.maintainer_additional_context = input.maintainerAdditionalContext;
return { scan_input: scanInput };
},
});
return normalizeConfiguration(raw);
}
async getStats(configurationId: string, signal?: AbortSignal): Promise<CodexSecurityCloudStats> {
const raw = await this.#request(`scan_configurations/${encodeURIComponent(configurationId)}/stats`, { signal });
const result: CodexSecurityCloudStats = {
configurationId,
pendingCommits: finiteNumber(raw.pending_commits),
finishedCommits: finiteNumber(raw.finished_commits),
failedCommits: finiteNumber(raw.failed_commits),
findingCounts: {
critical: finiteNumber(raw.critical_findings),
high: finiteNumber(raw.high_findings),
medium: finiteNumber(raw.medium_findings),
low: finiteNumber(raw.low_findings),
informational: finiteNumber(raw.informational_findings),
},
};
const sourceConfigurationId = optionalString(raw.config_id);
if (sourceConfigurationId && sourceConfigurationId !== configurationId) {
result.sourceConfigurationId = sourceConfigurationId;
}
const currentStep = optionalString(raw.current_step);
if (currentStep) result.currentStep = currentStep;
const lastScannedCommit = optionalString(raw.last_scanned_commit_hash);
if (lastScannedCommit) result.lastScannedCommit = lastScannedCommit;
const lastScannedAt = optionalString(raw.last_scanned_commit_dt);
if (lastScannedAt) result.lastScannedAt = lastScannedAt;
const updatedAt = optionalString(raw.updated_at);
if (updatedAt) result.updatedAt = updatedAt;
return result;
}
async listFindingDetails(
repositoryUrl: string,
configuration: CodexSecurityCloudConfiguration,
signal?: AbortSignal,
): Promise<JsonObject[]> {
const summaries: JsonObject[] = [];
let cursor: string | undefined;
do {
const page = await this.#request("scan-findings", {
query: {
repo: repositoryUrl,
limit: 500,
cursor,
status: ALL_FINDING_STATUSES.join(","),
},
signal,
});
if (Array.isArray(page.items)) summaries.push(...page.items.map(object));
cursor = optionalString(page.next_cursor);
} while (cursor);
const configurationIds = new Set([configuration.id, configuration.sourceId].filter((id): id is string => !!id));
const selected = summaries.filter(item => {
const configuredScanId = optionalString(item.configured_scan_id);
return configuredScanId === undefined || configurationIds.has(configuredScanId);
});
const details: JsonObject[] = [];
for (let index = 0; index < selected.length; index += 8) {
const batch = selected.slice(index, index + 8);
details.push(
...(await Promise.all(
batch.map(item => {
const id = requiredString(item.hid ?? item.id, "finding id");
return this.#request(`scan-findings/${encodeURIComponent(id)}`, { signal });
}),
)),
);
}
return details;
}
}
function normalizePath(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const normalized = value.trim().replaceAll("\\", "/").replace(/^\.\//, "");
if (
!normalized ||
normalized.startsWith("/") ||
/^[a-zA-Z]:\//.test(normalized) ||
normalized.split("/").includes("..")
)
return undefined;
return normalized;
}
function severity(value: unknown): SecuritySeverityLevel {
return value === "critical" || value === "high" || value === "medium" || value === "low" || value === "informational"
? value
: "informational";
}
function confidence(commit: JsonObject): SecurityConfidenceLevel {
const value = commit.validation_confidence;
if (typeof value === "number") return value >= 0.67 ? "high" : value >= 0.34 ? "medium" : "low";
return commit.validated === true ? "high" : "medium";
}
function disposition(value: unknown): SecurityDispositionStatus {
switch (value) {
case "fixed":
return "fixed";
case "false_positive":
return "false_positive";
case "wontfix":
return "wont_fix";
case "duplicate":
return "accepted_risk";
default:
return "open";
}
}
function text(value: unknown): string | undefined {
if (typeof value === "string" && value.length > 0) return value;
return undefined;
}
function locationsAndEvidence(
commit: JsonObject,
fingerprintSeed: string,
): { locations: SecurityLocation[]; evidence: SecurityEvidence[] } {
const relevantLines = Array.isArray(commit.relevant_lines) ? commit.relevant_lines : [];
const locations: SecurityLocation[] = [];
const evidenceInputs: Array<{
label: string;
explanation: string;
excerpt?: string;
location?: SecurityLocation;
kind: SecurityEvidence["kind"];
}> = [];
for (const [index, value] of relevantLines.entries()) {
const line = optionalObject(value);
const sourcePath = normalizePath(line.path);
const startLine = positiveInteger(line.start_line_number);
if (!sourcePath || !startLine) continue;
const location: SecurityLocation = { path: sourcePath, startLine };
const endLine = positiveInteger(line.end_line_number);
if (endLine && endLine >= startLine) location.endLine = endLine;
locations.push(location);
const entry: (typeof evidenceInputs)[number] = {
kind: "code",
label: `Cloud source evidence ${index + 1}`,
explanation: text(line.comment) ?? "Source location reported by Codex Security cloud.",
location,
};
const excerpt = text(line.content);
if (excerpt) entry.excerpt = excerpt;
evidenceInputs.push(entry);
}
if (locations.length === 0 && Array.isArray(commit.files_involved)) {
for (const value of commit.files_involved) {
const sourcePath = normalizePath(value);
if (sourcePath) locations.push({ path: sourcePath, startLine: 1, role: "cloud-file" });
}
}
if (locations.length === 0)
throw new Error("Codex Security cloud finding has no usable repository-relative location");
const validationReport = text(commit.validation_report) ?? text(commit.fix_check_report);
if (validationReport) {
evidenceInputs.push({
kind: "validation",
label: "Cloud validation",
explanation: validationReport,
});
}
const evidence = evidenceInputs.map((item, index) => ({
id: createSecurityEvidenceId(fingerprintSeed, item.label, index),
...item,
}));
return { locations, evidence };
}
function normalizeFinding(
raw: JsonObject,
scanId: string,
configuration: CodexSecurityCloudConfiguration,
importedAt: string,
): SecurityFinding {
const commit = optionalObject(raw.commit_analysis);
const title = requiredString(commit.title ?? raw.title, "finding title");
const ruleId = optionalString(commit.rule_id) ?? `codex-security:${sha256(title.trim().toLowerCase()).slice(0, 16)}`;
const category = optionalString(commit.category) ?? "codex-security";
const cloudId = requiredString(raw.hid ?? raw.id, "finding id");
const preliminary = locationsAndEvidence(commit, cloudId);
const fingerprint = createSecurityFindingFingerprint({ ruleId, category, locations: preliminary.locations });
const evidence = preliminary.evidence.map((item, index) => ({
...item,
id: createSecurityEvidenceId(fingerprint, item.label, index),
}));
const validationEvidenceIds = evidence.filter(item => item.kind === "validation").map(item => item.id);
const createdAt = optionalString(raw.created_at) ?? importedAt;
const producer = { ...CLOUD_PRODUCER };
const sourceIds: Record<string, string> = { cloudConfigurationId: configuration.id, cloudFindingId: cloudId };
for (const [key, value] of [
["cloudSourceFindingId", raw.id],
["cloudScanId", raw.scan_id],
["cloudJobId", raw.job_id],
] as const) {
if (typeof value === "string" && value.length > 0) sourceIds[key] = value;
}
const upstream: NonNullable<SecurityProvenance["upstream"]> = { repository: configuration.repositoryUrl };
const revision = optionalString(commit.commit_hash);
if (revision) upstream.revision = revision;
const provenance: SecurityProvenance = {
producer,
createdAt,
importedAt,
sourceIds,
upstream,
metadata: {
cloudStatus: raw.status ?? null,
bugStatus: commit.bug_status ?? null,
securityRelated: commit.security_related ?? null,
validationMethod: commit.validation_method ?? null,
},
};
const findingSeverity: SecurityFinding["severity"] = { level: severity(raw.criticality ?? commit.criticality) };
const severityRationale = text(raw.criticality_reason);
if (severityRationale) findingSeverity.rationale = severityRationale;
const validation: SecurityFinding["validation"] = {
status: commit.validated === true ? "validated" : "unvalidated",
evidenceIds: validationEvidenceIds,
};
const validatedAt = optionalString(commit.validation_finished_at);
if (validatedAt) validation.validatedAt = validatedAt;
const validationSummary = text(commit.validation_report);
if (validationSummary) validation.summary = validationSummary;
const findingDisposition: SecurityFinding["disposition"] = { status: disposition(raw.status) };
const dispositionRationale = text(raw.resolution_reason);
if (dispositionRationale) findingDisposition.rationale = dispositionRationale;
const dispositionUpdatedAt = optionalString(raw.updated_at);
if (dispositionUpdatedAt) findingDisposition.updatedAt = dispositionUpdatedAt;
const finding: SecurityFinding = {
id: createSecurityFindingId(fingerprint),
scanId,
fingerprint,
ruleId,
title,
summary: text(commit.description) ?? text(raw.description) ?? title,
severity: findingSeverity,
confidence: { level: confidence(commit) },
taxonomy: { category, cwe: [] },
occurrences: [
{
id: createSecurityOccurrenceId(fingerprint, preliminary.locations),
locations: preliminary.locations,
evidenceIds: evidence.filter(item => item.kind === "code").map(item => item.id),
},
],
evidence,
validation,
disposition: findingDisposition,
provenance,
extensions: {
cloudFindingVersion: raw.version ?? null,
cloudValidationConfidence: commit.validation_confidence ?? null,
},
};
const remediation = text(commit.proposed_patch) ?? text(raw.proposed_patch);
if (remediation) finding.remediation = remediation;
return finding;
}
function repositoryIdentity(value: string): string {
const trimmed = value
.trim()
.replace(/\/+$/, "")
.replace(/\.git$/, "");
const scpStyle = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
if (scpStyle) return `${scpStyle[1]!.toLowerCase()}/${scpStyle[2]!.replace(/^\/+/, "").toLowerCase()}`;
try {
const parsed = new URL(trimmed);
return `${parsed.hostname.toLowerCase()}/${parsed.pathname.replace(/^\/+/, "").toLowerCase()}`;
} catch {
return trimmed.toLowerCase();
}
}
async function assertCloudRepositoryMatchesStore(
configuration: CodexSecurityCloudConfiguration,
store: SecurityStore,
signal?: AbortSignal,
): Promise<void> {
const origin = await git.remote.url(store.repositoryRoot, "origin", signal);
if (!origin) return;
if (repositoryIdentity(origin) !== repositoryIdentity(configuration.repositoryUrl)) {
throw new Error("Codex Security cloud configuration does not match this project's origin remote");
}
}
function reportForCloudBundle(
configuration: CodexSecurityCloudConfiguration,
stats: CodexSecurityCloudStats,
findings: SecurityFinding[],
): string {
const lines = [
"# Codex Security cloud results",
"",
`- Configuration: ${configuration.id}`,
`- Repository: ${configuration.repositoryUrl}`,
`- Current step: ${stats.currentStep ?? configuration.currentStep ?? "unknown"}`,
`- Last scanned commit: ${stats.lastScannedCommit ?? "unknown"}`,
`- Findings imported: ${findings.length}`,
"",
"## Findings",
"",
];
for (const finding of findings) lines.push(`- **${finding.severity.level}** ${finding.title} (${finding.id})`);
return `${lines.join("\n")}\n`;
}
export async function pullCodexSecurityCloudResults(
input: PullCodexSecurityCloudResultsInput,
): Promise<SecurityScanBundle> {
const importedAt = new Date().toISOString();
const configuration = await input.client.getConfiguration(input.configurationId, input.signal);
const stats = await input.client.getStats(configuration.id, input.signal);
await assertCloudRepositoryMatchesStore(configuration, input.store, input.signal);
const details = await input.client.listFindingDetails(configuration.repositoryUrl, configuration, input.signal);
const scanId = createSecurityScanId();
const findings = details.map(item => normalizeFinding(item, scanId, configuration, importedAt));
const scanSourceIds: Record<string, string> = { cloudConfigurationId: configuration.id };
if (configuration.sourceId) scanSourceIds.cloudSourceConfigurationId = configuration.sourceId;
const producer = { ...CLOUD_PRODUCER };
const revision = stats.lastScannedCommit;
const bundle: SecurityScanBundle = {
scan: {
documentType: "omp-security.scan",
schemaVersion: "1.0",
id: scanId,
projectKey: input.store.projectKey,
status: "completed",
createdAt: importedAt,
completedAt: importedAt,
target: {
kind: "imported",
repositoryRoot: input.store.repositoryRoot,
displayName: configuration.repositoryUrl,
includePaths: [],
excludePaths: [],
treeDigest: sha256(`${configuration.repositoryUrl}\0${revision ?? "unknown"}`),
},
producer,
provenance: {
producer,
createdAt: configuration.createdAt ?? importedAt,
importedAt,
sourceIds: scanSourceIds,
upstream: { repository: configuration.repositoryUrl },
metadata: {
cloudCurrentStep: stats.currentStep ?? configuration.currentStep ?? null,
cloudUpdatedAt: stats.updatedAt ?? configuration.updatedAt ?? null,
},
},
findingIds: findings.map(item => item.id),
coverage: {
mode: "imported",
completeness: "unknown",
inventoryStrategy: "imported",
includePaths: [],
excludePaths: [],
surfaces: [],
explicitExclusions: [],
deferred: [
{ id: "cloud-coverage", reason: "Cloud coverage receipts are not exposed by the findings API." },
],
},
reportRef: "report.md",
sarifRef: "results.sarif",
},
findings,
report: reportForCloudBundle(configuration, stats, findings),
};
if (configuration.createdAt) bundle.scan.startedAt = configuration.createdAt;
if (revision) {
bundle.scan.target.revision = revision;
if (bundle.scan.provenance.upstream) bundle.scan.provenance.upstream.revision = revision;
}
bundle.sarif = exportSecurityBundleToSarif(bundle);
await input.store.putBundle(bundle);
return bundle;
}
@@ -4,7 +4,7 @@ const stringRecordSchema = type({ "[string]": "string" });
const unknownRecordSchema = type({ "[string]": "unknown" });
export const securityProducerSchema = type({
kind: "'omp-native' | 'codex-security-bundle' | 'sarif-import'",
kind: "'omp-native' | 'codex-security-bundle' | 'codex-security-cloud' | 'sarif-import'",
name: "string > 0",
"version?": "string",
"vendor?": "string",
@@ -5,7 +5,7 @@ export type SecurityCoverageCompleteness = "complete" | "partial" | "unknown";
export type SecurityValidationStatus = "unvalidated" | "validated" | "rejected" | "partial" | "error";
export type SecurityDispositionStatus = "open" | "false_positive" | "accepted_risk" | "fixed" | "wont_fix";
export type SecurityTargetKind = "repository" | "scoped_path" | "ref_diff" | "working_tree" | "imported";
export type SecurityProducerKind = "omp-native" | "codex-security-bundle" | "sarif-import";
export type SecurityProducerKind = "omp-native" | "codex-security-bundle" | "codex-security-cloud" | "sarif-import";
export interface SecurityProducer {
kind: SecurityProducerKind;
@@ -15,9 +15,8 @@ import type { AgentSession } from "../session/agent-session";
import type { AuthStorage } from "../session/auth-storage";
import { SessionManager } from "../session/session-manager";
import * as git from "../utils/git";
import { createExactSecurityOAuthResolver } from "./auth";
import { createExactSecurityOAuthResolver, selectSecurityAccount } from "./auth";
import type {
SecurityAccountRef,
SecurityCoverage,
SecurityModelRef,
SecurityScan,
@@ -224,38 +223,6 @@ function initialBundle(
};
}
function resolveAccount(
host: SecurityCoordinatorHost,
model: Model,
requestedCredentialId?: number,
): SecurityAccountRef {
const accounts = host.authStorage.listOAuthAccounts(model.provider, host.sessionId);
const selected =
requestedCredentialId !== undefined
? accounts.find(account => account.credentialId === requestedCredentialId)
: (accounts.find(account => account.active) ?? (accounts.length === 1 ? accounts[0] : undefined));
if (!selected) {
if (accounts.length === 0) {
throw new Error(`Security scans require a stored OAuth account for ${model.provider}`);
}
if (requestedCredentialId !== undefined) {
throw new Error(`Security OAuth credential ${requestedCredentialId} is not available for ${model.provider}`);
}
throw new Error(
`Multiple OAuth accounts are available for ${model.provider}; supply credentialId to pin one exact account`,
);
}
const account: SecurityAccountRef = {
provider: model.provider,
credentialId: selected.credentialId,
};
if (selected.accountId !== undefined) account.accountId = selected.accountId;
if (selected.email !== undefined) account.email = selected.email;
if (selected.orgId !== undefined) account.organizationId = selected.orgId;
if (selected.orgName !== undefined) account.organizationName = selected.orgName;
return account;
}
async function createDefaultSecuritySession(input: SecurityScanSessionFactoryInput): Promise<AgentSession> {
const scanSettings = await input.host.settings.cloneForCwd(input.executionRoot);
const modelSelector = `${input.model.provider}/${input.model.id}`;
@@ -448,7 +415,12 @@ export class SecurityCoordinator {
}
const model = input.model ?? this.#host.activeModel;
if (!model) throw new Error("Security scan preflight requires an active model");
const account = resolveAccount(this.#host, model, input.credentialId);
const account = selectSecurityAccount(
this.#host.authStorage,
model.provider,
input.credentialId,
this.#host.sessionId,
);
const store = await this.#openStore(this.#host.cwd);
const workRoot = path.join(store.projectDirectory, "work");
await fs.mkdir(workRoot, { recursive: true, mode: 0o700 });
@@ -1,4 +1,5 @@
export * from "./auth";
export * from "./cloud";
export * from "./comparison";
export * from "./contracts";
export * from "./coordinator";
@@ -4,6 +4,8 @@ import { prompt } from "@oh-my-pi/pi-utils";
import { parseInternalUrl } from "../../internal-urls/parse";
import { SecurityProtocolHandler } from "../../internal-urls/security-protocol";
import validationRequestPrompt from "../../prompts/security/validate-request.md" with { type: "text" };
import { selectSecurityAccount } from "../../security/auth";
import { CodexSecurityCloudClient, pullCodexSecurityCloudResults } from "../../security/cloud";
import type { SecurityDispositionStatus } from "../../security/contracts";
import type { SecurityPreflightInput } from "../../security/coordinator";
import { getSecurityCoordinator } from "../../security/coordinator";
@@ -203,6 +205,130 @@ async function exportResults(runtime: SlashCommandRuntime, rest: string): Promis
await runtime.output(`Exported security scan ${scanId} to ${absolute}.`);
}
interface CloudCliOptions {
credentialId?: number;
configurationId?: string;
repositoryId?: string;
repositoryUrl?: string;
environmentId?: string;
lookbackDays?: number | "all";
}
function parseCloudOptions(rest: string, subcommand: string): CloudCliOptions {
const tokens = parseCommandArgs(rest);
const options: CloudCliOptions = {};
let positionalConsumed = false;
for (let index = 0; index < tokens.length; index++) {
const token = tokens[index]!;
switch (token) {
case "--credential":
options.credentialId = parsePositiveCredential(requireToken(tokens, ++index, token));
break;
case "--repo-id":
options.repositoryId = requireToken(tokens, ++index, token);
break;
case "--repo-url":
options.repositoryUrl = requireToken(tokens, ++index, token);
break;
case "--environment":
options.environmentId = requireToken(tokens, ++index, token);
break;
case "--lookback": {
const value = requireToken(tokens, ++index, token);
if (value === "all") {
options.lookbackDays = value;
break;
}
const days = Number(value);
if (!Number.isSafeInteger(days) || days < 1) throw new Error(`Invalid lookback: ${value}`);
options.lookbackDays = days;
break;
}
default:
if (!token.startsWith("--") && !positionalConsumed && (subcommand === "status" || subcommand === "pull")) {
options.configurationId = token;
positionalConsumed = true;
break;
}
throw new Error(`Unknown security cloud option: ${token}`);
}
}
return options;
}
function cloudClientFor(runtime: SlashCommandRuntime, credentialId?: number): CodexSecurityCloudClient {
const authStorage = runtime.session.modelRegistry.authStorage;
const account = selectSecurityAccount(authStorage, "openai-codex", credentialId, runtime.session.sessionId);
return new CodexSecurityCloudClient({ authStorage, account });
}
async function handleCloudCommand(runtime: SlashCommandRuntime, rest: string): Promise<void> {
const { verb, rest: optionsText } = parseSubcommand(rest);
const subcommand = verb || "scans";
const options = parseCloudOptions(optionsText, subcommand);
const client = cloudClientFor(runtime, options.credentialId);
switch (subcommand) {
case "scans": {
const configurations = await client.listAllConfigurations();
await runtime.output(
configurations.length === 0
? "No Codex Security cloud scan configurations are available for this account."
: configurations
.map(item =>
[
item.id,
item.state ?? "unknown",
item.currentStep ?? "unknown",
`repo=${item.repositoryId}`,
`environment=${item.environmentId}`,
item.repositoryUrl,
item.remainingScans === undefined ? "" : `${item.remainingScans} scan(s) remaining`,
]
.filter(Boolean)
.join(" "),
)
.join("\n"),
);
return;
}
case "start": {
if (!options.repositoryId || !options.repositoryUrl || !options.environmentId) {
throw new Error("cloud start requires --repo-id, --repo-url, and --environment");
}
const configuration = await client.startScan({
repositoryId: options.repositoryId,
repositoryUrl: options.repositoryUrl,
environmentId: options.environmentId,
lookbackDays: options.lookbackDays,
});
await runtime.output(
`Codex Security cloud scan ${configuration.id} started for ${configuration.repositoryUrl}. This consumes cloud scan allowance.`,
);
return;
}
case "status": {
if (!options.configurationId) throw new Error("cloud status requires a configuration id");
await runtime.output(JSON.stringify(await client.getStats(options.configurationId), null, 2));
return;
}
case "pull": {
if (!options.configurationId) throw new Error("cloud pull requires a configuration id");
const store = await SecurityStore.openForCwd(runtime.cwd);
const bundle = await pullCodexSecurityCloudResults({
client,
configurationId: options.configurationId,
store,
});
await runtime.output(
`Imported ${bundle.findings.length} Codex Security cloud finding(s) as security scan ${bundle.scan.id}.`,
);
return;
}
default:
throw new Error("Usage: /security cloud <scans|start|status|pull>");
}
}
async function updateDisposition(runtime: SlashCommandRuntime, rest: string): Promise<void> {
const [scanId, findingId, status, ...rationaleParts] = parseCommandArgs(rest);
if (!scanId || !findingId || !status) {
@@ -304,12 +430,15 @@ export async function handleSecurityCommand(
await runtime.output(JSON.stringify(report, null, 2));
return commandConsumed();
}
case "cloud":
await handleCloudCommand(runtime, rest);
return commandConsumed();
case "disposition":
await updateDisposition(runtime, rest);
return commandConsumed();
default:
return usage(
"Usage: /security <plan|scan|status|cancel|scans|show|import|export|validate|compare|disposition>",
"Usage: /security <plan|scan|status|cancel|scans|cloud|show|import|export|validate|compare|disposition>",
runtime,
);
}
@@ -1,6 +1,13 @@
import type { AgentTool, AgentToolResult, ToolTier } from "@oh-my-pi/pi-agent-core";
import { type } from "arktype";
import securityScanDescription from "../prompts/tools/security-scan.md" with { type: "text" };
import { selectSecurityAccount } from "../security/auth";
import {
CodexSecurityCloudClient,
type CodexSecurityCloudConfiguration,
type CodexSecurityCloudStats,
pullCodexSecurityCloudResults,
} from "../security/cloud";
import { createSecurityEvidenceId, type SecurityEvidence, type SecurityValidationStatus } from "../security/contracts";
import type { SecurityOperationSnapshot } from "../security/coordinator";
import { getSecurityCoordinator } from "../security/coordinator";
@@ -10,7 +17,8 @@ import type { ToolSession } from "./index";
import { ToolError } from "./tool-errors";
const securityScanSchema = type({
action: "'preflight' | 'start' | 'status' | 'cancel' | 'validate'",
action:
"'preflight' | 'start' | 'status' | 'cancel' | 'validate' | 'cloud_scans' | 'cloud_start' | 'cloud_status' | 'cloud_pull'",
"plan_id?": "string",
"operation_id?": "string",
"target_kind?": "'repository' | 'scoped_path' | 'ref_diff' | 'working_tree'",
@@ -27,6 +35,11 @@ const securityScanSchema = type({
"validation_status?": "'unvalidated' | 'validated' | 'rejected' | 'partial' | 'error'",
"validation_summary?": "string",
"validation_evidence?": type({ label: "string > 0", explanation: "string" }).array(),
"cloud_configuration_id?": "string",
"repository_id?": "string",
"repository_url?": "string",
"environment_id?": "string",
"lookback_days?": "number.integer >= 1 | 'all'",
});
type SecurityScanParams = typeof securityScanSchema.infer;
@@ -37,6 +50,10 @@ export interface SecurityScanToolDetails {
operation?: SecurityOperationSnapshot;
cancelled?: boolean;
finding?: { id: string; validationStatus: SecurityValidationStatus };
cloudConfigurations?: CodexSecurityCloudConfiguration[];
cloudStats?: CodexSecurityCloudStats;
cloudScan?: { id: string; repositoryUrl: string };
importedScan?: { id: string; findingCount: number };
}
function targetFromParams(params: SecurityScanParams): SecurityTargetRequest {
@@ -70,6 +87,17 @@ function requireValue(value: string | undefined, label: string): string {
return value.trim();
}
function cloudClientForSession(session: ToolSession, credentialId?: number): CodexSecurityCloudClient {
if (!session.authStorage) throw new ToolError("Codex Security cloud requires the authentication registry");
const account = selectSecurityAccount(
session.authStorage,
"openai-codex",
credentialId,
session.getSessionId?.() ?? undefined,
);
return new CodexSecurityCloudClient({ authStorage: session.authStorage, account });
}
function textResult(text: string, details: SecurityScanToolDetails): AgentToolResult<SecurityScanToolDetails> {
return { content: [{ type: "text", text }], details };
}
@@ -79,7 +107,7 @@ export class SecurityScanTool implements AgentTool<typeof securityScanSchema, Se
readonly approval: ToolTier = "exec";
readonly label = "Security Scan";
readonly loadMode = "discoverable";
readonly summary = "Plan, run, and validate an OMP-native software-security scan";
readonly summary = "Run OMP-native scans and explicit Codex Security cloud operations";
readonly description = securityScanDescription.trim();
readonly parameters = securityScanSchema;
readonly strict = true;
@@ -160,6 +188,65 @@ export class SecurityScanTool implements AgentTool<typeof securityScanSchema, Se
},
);
}
case "cloud_scans": {
const configurations = await cloudClientForSession(
this.session,
params.credential_id,
).listAllConfigurations(signal);
return textResult(
configurations.length === 0
? "No Codex Security cloud scan configurations are available."
: configurations
.map(
item =>
`${item.id} ${item.currentStep ?? "unknown"} repo=${item.repositoryId} environment=${item.environmentId} ${item.repositoryUrl}`,
)
.join("\n"),
{ action: params.action, cloudConfigurations: configurations },
);
}
case "cloud_start": {
const configuration = await cloudClientForSession(this.session, params.credential_id).startScan({
repositoryId: requireValue(params.repository_id, "repository_id"),
repositoryUrl: requireValue(params.repository_url, "repository_url"),
environmentId: requireValue(params.environment_id, "environment_id"),
lookbackDays: params.lookback_days,
signal,
});
return textResult(
`Codex Security cloud scan ${configuration.id} started for ${configuration.repositoryUrl}. This consumes cloud scan allowance.`,
{
action: params.action,
cloudScan: { id: configuration.id, repositoryUrl: configuration.repositoryUrl },
},
);
}
case "cloud_status": {
const stats = await cloudClientForSession(this.session, params.credential_id).getStats(
requireValue(params.cloud_configuration_id, "cloud_configuration_id"),
signal,
);
return textResult(
`Codex Security cloud scan ${stats.configurationId}: ${stats.currentStep ?? "unknown"}; ${stats.finishedCommits} finished commit(s), ${stats.pendingCommits} pending.`,
{ action: params.action, cloudStats: stats },
);
}
case "cloud_pull": {
const store = await SecurityStore.openForCwd(this.session.cwd, { signal });
const bundle = await pullCodexSecurityCloudResults({
client: cloudClientForSession(this.session, params.credential_id),
configurationId: requireValue(params.cloud_configuration_id, "cloud_configuration_id"),
store,
signal,
});
return textResult(
`Imported ${bundle.findings.length} Codex Security cloud finding(s) as security scan ${bundle.scan.id}.`,
{
action: params.action,
importedScan: { id: bundle.scan.id, findingCount: bundle.findings.length },
},
);
}
case "validate": {
const scanId = requireValue(params.scan_id, "scan_id");
const findingId = requireValue(params.finding_id, "finding_id");
@@ -1,7 +1,7 @@
import { describe, expect, test, vi } from "bun:test";
import type { ApiKeyResolver } from "@oh-my-pi/pi-ai/auth-retry";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import { createExactSecurityOAuthResolver } from "../../src/security";
import { createExactSecurityOAuthResolver, selectSecurityAccount } from "../../src/security";
import type { AuthStorage } from "../../src/session/auth-storage";
function model() {
@@ -11,6 +11,21 @@ function model() {
}
describe("exact security OAuth resolver", () => {
test("selects an explicit credential without account rotation", () => {
const listOAuthAccounts = vi.fn(() => [
{ credentialId: 11, position: 0, active: true, accountId: "workspace-a" },
{ credentialId: 42, position: 1, active: false, accountId: "workspace-b" },
]);
const selected = selectSecurityAccount(
{ listOAuthAccounts } as unknown as AuthStorage,
"openai-codex",
42,
"session-a",
);
expect(selected).toEqual({ provider: "openai-codex", credentialId: 42, accountId: "workspace-b" });
expect(listOAuthAccounts).toHaveBeenCalledWith("openai-codex", "session-a");
});
test("resolves and refreshes only the pinned durable row", async () => {
const getOAuthAccessByCredentialId = vi.fn(async (_provider, credentialId, options) => ({
ok: true as const,
@@ -0,0 +1,302 @@
import { describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { $ } from "bun";
import type { CodexSecurityCloudFetch } from "../../src/security";
import {
CodexSecurityCloudClient,
CodexSecurityCloudHttpError,
pullCodexSecurityCloudResults,
SecurityStore,
} from "../../src/security";
import type { AuthStorage } from "../../src/session/auth-storage";
const ACCOUNT = { provider: "openai-codex", credentialId: 42, accountId: "workspace-a" } as const;
function jwt(subject = "user-a"): string {
return `header.${Buffer.from(JSON.stringify({ sub: subject })).toString("base64url")}.signature`;
}
function json(value: unknown, status = 200): Response {
return new Response(JSON.stringify(value), { status, headers: { "Content-Type": "application/json" } });
}
function authStorage(accessToken = jwt()): AuthStorage {
return {
getOAuthAccessByCredentialId: async (_provider: string, credentialId: number) => ({
ok: true as const,
accessToken,
credentialId,
accountId: "workspace-a",
}),
} as unknown as AuthStorage;
}
function configuration() {
return {
id: "config-source",
hid: "config-public",
created_at: "2026-07-29T00:00:00.000Z",
updated_at: "2026-07-29T00:05:00.000Z",
current_step: "waiting_for_new_commits",
scans_remaining: 4,
total_scans: 5,
scan_input: {
environment_id: "env-a",
repo_id: "repo-a",
repo_url: "https://github.com/example/repository",
state: "enabled",
},
};
}
describe("Codex Security cloud client", () => {
test("pins one account and refreshes the same credential once after a 401", async () => {
const resolutions: boolean[] = [];
const requests: Array<{ authorization: string | null; accountId: string | null }> = [];
const storage = {
getOAuthAccessByCredentialId: async (
_provider: string,
credentialId: number,
options: { forceRefresh: boolean },
) => {
resolutions.push(options.forceRefresh);
return {
ok: true as const,
accessToken: options.forceRefresh ? "refreshed-token" : "initial-token",
credentialId,
accountId: "workspace-a",
};
},
} as unknown as AuthStorage;
let attempt = 0;
const fetchMock: CodexSecurityCloudFetch = async (_input, init) => {
const headers = new Headers(init?.headers);
requests.push({
authorization: headers.get("Authorization"),
accountId: headers.get("ChatGPT-Account-Id"),
});
attempt += 1;
return attempt === 1 ? json({}, 401) : json({ items: [configuration()], total_in_account: 1 });
};
const client = new CodexSecurityCloudClient({
authStorage: storage,
account: ACCOUNT,
baseUrl: "https://example.test/backend-api/aardvark",
fetch: fetchMock,
});
const page = await client.listConfigurations();
expect(resolutions).toEqual([false, true]);
expect(requests).toEqual([
{ authorization: "Bearer initial-token", accountId: "workspace-a" },
{ authorization: "Bearer refreshed-token", accountId: "workspace-a" },
]);
expect(page.items[0]).toMatchObject({
id: "config-public",
sourceId: "config-source",
repositoryId: "repo-a",
environmentId: "env-a",
remainingScans: 4,
});
});
test("creates the documented cloud scan configuration without runtime attribution spoofing", async () => {
let requestUrl = "";
let requestBody: unknown;
const fetchMock: CodexSecurityCloudFetch = async (input, init) => {
requestUrl = String(input);
requestBody = JSON.parse(String(init?.body));
return json(configuration());
};
const client = new CodexSecurityCloudClient({
authStorage: authStorage(jwt("user-exact")),
account: ACCOUNT,
baseUrl: "https://example.test/backend-api/aardvark",
fetch: fetchMock,
});
await client.startScan({
repositoryId: "repo-a",
repositoryUrl: "https://github.com/example/repository",
environmentId: "env-a",
lookbackDays: "all",
});
expect(requestUrl).toBe("https://example.test/backend-api/aardvark/scan_configurations");
expect(requestBody).toEqual({
scan_input: {
environment_id: "env-a",
lookback_days: null,
notification_rules: [],
owner_id: "user-exact",
repo_id: "repo-a",
repo_url: "https://github.com/example/repository",
share_targets: [],
state: "enabled",
},
});
expect(JSON.stringify(requestBody)).not.toContain("codex_sdk_ts");
});
test("imports cloud findings into the canonical store and SARIF", async () => {
const repositoryRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-cloud-security-repo-"));
const stateRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-cloud-security-state-"));
const store = await SecurityStore.open(repositoryRoot, { stateRoot });
const fetchMock: CodexSecurityCloudFetch = async input => {
const url = new URL(String(input));
if (url.pathname.endsWith("/scan_configurations")) {
return json({ items: [configuration()], total_in_account: 1 });
}
if (url.pathname.endsWith("/scan_configurations/config-public/stats")) {
return json({
config_id: "config-source",
current_step: "waiting_for_new_commits",
pending_commits: 0,
finished_commits: 3,
failed_commits: 0,
critical_findings: 0,
high_findings: 1,
medium_findings: 0,
low_findings: 0,
informational_findings: 0,
last_scanned_commit_hash: "abc123",
last_scanned_commit_dt: "2026-07-29T00:04:00.000Z",
updated_at: "2026-07-29T00:05:00.000Z",
});
}
if (url.pathname.endsWith("/scan-findings")) {
expect(url.searchParams.get("status")).toBe(
"new,triaged,in_progress,fixed,wontfix,duplicate,false_positive",
);
return json({
items: [{ id: "finding-source", hid: "finding-public", configured_scan_id: "config-source" }],
next_cursor: null,
});
}
if (url.pathname.endsWith("/scan-findings/finding-public")) {
return json({
id: "finding-source",
hid: "finding-public",
configured_scan_id: "config-source",
scan_id: "cloud-scan-a",
job_id: "cloud-job-a",
created_at: "2026-07-29T00:02:00.000Z",
updated_at: "2026-07-29T00:03:00.000Z",
criticality: "high",
criticality_reason: "Attacker-controlled data reaches a command sink.",
status: "new",
version: 2,
commit_analysis: {
title: "Command injection",
description: "Untrusted input reaches shell execution.",
commit_hash: "abc123",
validated: true,
validation_confidence: 1,
validation_method: "crash",
validation_finished_at: "2026-07-29T00:03:00.000Z",
validation_report: "The exploit reproduced in an isolated environment.",
proposed_patch: "Use argument-array process execution.",
relevant_lines: [
{
path: "src/command.ts",
start_line_number: 7,
end_line_number: 9,
content: "exec(input)",
comment: "Untrusted input is interpolated into a shell command.",
},
],
},
});
}
throw new Error(`Unexpected request: ${url}`);
};
const client = new CodexSecurityCloudClient({
authStorage: authStorage(),
account: ACCOUNT,
baseUrl: "https://example.test/backend-api/aardvark",
fetch: fetchMock,
});
const bundle = await pullCodexSecurityCloudResults({ client, configurationId: "config-public", store });
expect(bundle.scan.producer.kind).toBe("codex-security-cloud");
expect(bundle.scan.target.revision).toBe("abc123");
expect(bundle.findings).toHaveLength(1);
expect(bundle.findings[0]).toMatchObject({
title: "Command injection",
severity: { level: "high" },
confidence: { level: "high" },
validation: { status: "validated" },
disposition: { status: "open" },
remediation: "Use argument-array process execution.",
});
expect(bundle.findings[0]!.occurrences[0]!.locations[0]).toEqual({
path: "src/command.ts",
startLine: 7,
endLine: 9,
});
expect(bundle.findings[0]!.evidence.map(item => item.kind)).toEqual(["code", "validation"]);
expect(bundle.sarif?.runs).toBeArray();
expect((await store.getBundle(bundle.scan.id))?.findings[0]?.provenance.sourceIds).toMatchObject({
cloudConfigurationId: "config-public",
cloudFindingId: "finding-public",
cloudScanId: "cloud-scan-a",
});
expect(JSON.stringify(bundle)).not.toContain("workspace-a");
});
test("refuses to import a cloud configuration for another repository", async () => {
const repositoryRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-cloud-security-mismatch-repo-"));
const stateRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-cloud-security-mismatch-state-"));
await $`git init --initial-branch=main`.cwd(repositoryRoot).quiet();
await $`git remote add origin https://github.com/example/different-repository.git`.cwd(repositoryRoot).quiet();
const store = await SecurityStore.open(repositoryRoot, { stateRoot });
const fetchMock: CodexSecurityCloudFetch = async input => {
const url = new URL(String(input));
if (url.pathname.endsWith("/scan_configurations")) {
return json({ items: [configuration()], total_in_account: 1 });
}
if (url.pathname.endsWith("/scan_configurations/config-public/stats")) {
return json({
config_id: "config-source",
current_step: "waiting_for_new_commits",
pending_commits: 0,
finished_commits: 1,
failed_commits: 0,
});
}
throw new Error(`Finding data should not be fetched for a mismatched repository: ${url}`);
};
const client = new CodexSecurityCloudClient({
authStorage: authStorage(),
account: ACCOUNT,
baseUrl: "https://example.test/backend-api/aardvark",
fetch: fetchMock,
});
await expect(pullCodexSecurityCloudResults({ client, configurationId: "config-public", store })).rejects.toThrow(
"does not match this project's origin remote",
);
});
test("returns a sanitized error without reflecting response bodies", async () => {
const client = new CodexSecurityCloudClient({
authStorage: authStorage(),
account: ACCOUNT,
baseUrl: "https://example.test/backend-api/aardvark",
fetch: async () => new Response("secret backend detail", { status: 403 }),
});
let caught: unknown;
try {
await client.listConfigurations();
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(CodexSecurityCloudHttpError);
if (!(caught instanceof Error)) throw new Error("expected cloud HTTP error");
expect(caught.message).not.toContain("secret backend detail");
});
});