Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6f11616
refactor: share persisted finding normalization
mldangelo-oai Oct 10, 2026
3432ed4
refactor: share workbench argument encoding
mldangelo-oai Oct 10, 2026
bb0ed9b
refactor(deep-scan): derive persisted response types from schemas
mldangelo-oai Oct 11, 2026
133018f
refactor(mcp): derive artifact and input contracts
mldangelo-oai Oct 11, 2026
8705f07
test: share saved-log bundle across Node capability cases
mldangelo-oai Oct 11, 2026
6e7e4a0
Merge remote-tracking branch 'origin/main' into mdangelo/codex/shared…
mldangelo-oai Oct 11, 2026
722b903
Merge branch 'mdangelo/codex/shared-finding-normalization' into mdang…
mldangelo-oai Oct 11, 2026
15c810e
refactor: reuse shared argv for deep scan mutations
mldangelo-oai Oct 11, 2026
cc309b0
Merge branch 'mdangelo/codex/shared-workbench-messages' into mdangelo…
mldangelo-oai Oct 11, 2026
8672506
Merge commit '825e7fd059a8047eef6fac6c814dce294d58fc18' into mdangelo…
mldangelo-oai Oct 11, 2026
427728c
Merge branch 'mdangelo/codex/shared-finding-normalization' into mdang…
mldangelo-oai Oct 11, 2026
bd95155
Merge branch 'mdangelo/codex/shared-workbench-messages' into mdangelo…
mldangelo-oai Oct 11, 2026
21b16ce
chore: merge main and resolve integration conflicts
mldangelo-oai Oct 11, 2026
f268ab2
Merge current finding normalization and preserve package validation
mldangelo-oai Oct 12, 2026
bf93c30
Merge workbench contracts and preserve scoped worker paths
mldangelo-oai Oct 12, 2026
1848a58
Merge main into shared workbench argument refactor
codex Oct 13, 2026
7e575d5
test(deep-scan): forward publication identity in terminal races
codex Oct 13, 2026
63bb0d2
Merge target branch into Deep Scan response contracts
codex Oct 13, 2026
c3dd9d1
Merge main into Deep Scan response contracts
codex Oct 16, 2026
58dfff4
test: isolate scan log bundling from coverage state
codex Oct 16, 2026
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
142 changes: 56 additions & 86 deletions plugins/codex-security/mcp-app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ import type { DeepScanRunState } from "./src/deep-scan/types.js";
import { PLUGIN_ROOT, runWorkbench, scanRoot } from "./src/workbench-client.js";

const USER_INPUT_WAIT_TIMEOUT_MS = 14 * 60 * 1000;

const userContextSchema = z.string().trim().min(1);
const editableUserContextSchema = z.string().trim();
const trimmedText = z.string().trim();
const requiredText = trimmedText.min(1);
const nonnegativeInteger = z.number().int().nonnegative();
const positiveInteger = z.number().int().positive();
const pathSchema = z
.string()
.min(1)
Expand Down Expand Up @@ -89,24 +90,24 @@ const daybreakEntitlementContextSchema = z.object({
const diffTargetSchema = z.discriminatedUnion("kind", [
z.strictObject({
kind: z.literal("working_tree"),
baseRevision: z.string().trim().min(1).max(512).optional(),
contentDigest: z.string().trim().min(1).max(128).optional(),
headRevision: z.string().trim().min(1).max(512).optional(),
baseRevision: requiredText.max(512).optional(),
contentDigest: requiredText.max(128).optional(),
headRevision: requiredText.max(512).optional(),
}),
z.strictObject({
kind: z.literal("commit"),
baseRevision: z.string().trim().min(1).max(512).optional(),
headRevision: z.string().trim().min(1).max(512),
baseRevision: requiredText.max(512).optional(),
headRevision: requiredText.max(512),
}),
z.strictObject({
kind: z.literal("range"),
baseRevision: z.string().trim().min(1).max(512),
headRevision: z.string().trim().min(1).max(512),
baseRevision: requiredText.max(512),
headRevision: requiredText.max(512),
}),
]);
const currentScanPreflightCheckSchema = z.strictObject({
capability: z.string().trim().min(1).max(128),
reason: z.string().trim().min(1).max(1200),
capability: requiredText.max(128),
reason: requiredText.max(1200),
severity: z.enum(["block", "warn", "suggest"]),
status: z.enum(["pass", "fail", "unknown"]),
});
Expand All @@ -133,29 +134,23 @@ const openSchema = {
targetPath: pathSchema
.optional()
.describe("Optional resolved local target path."),
targetSummary: z
.string()
.trim()
.min(1)
targetSummary: requiredText
.max(2400)
.optional()
.describe("Optional bounded target/security context."),
targetTitle: z
.string()
.trim()
.min(1)
targetTitle: requiredText
.max(200)
.optional()
.describe("Optional human-readable target name."),
userContext: userContextSchema
userContext: requiredText
.optional()
.describe("Optional security focus supplied by the user."),
};
const sessionSchema = { sessionId: z.string().uuid() };
const startScanSchema = {
...sessionSchema,
model: z.string().trim().min(1).max(200).optional(),
reasoningEffort: z.string().trim().min(1).max(32).optional(),
model: requiredText.max(200).optional(),
reasoningEffort: requiredText.max(32).optional(),
};
const startPromptOnlyScanSchema = {
diffTarget: diffTargetSchema
Expand All @@ -170,14 +165,11 @@ const startPromptOnlyScanSchema = {
"Directory inside targetPath. Use '.' for the whole target.",
),
targetPath: pathSchema.describe("Resolved local target path."),
targetSummary: z
.string()
.trim()
.min(1)
targetSummary: requiredText
.max(2400)
.optional()
.describe("Optional bounded target or change-set context."),
userContext: userContextSchema
userContext: requiredText
.optional()
.describe("Optional security focus supplied by the user."),
};
Expand All @@ -188,31 +180,24 @@ const startHeadlessStandardScanSchema = {
.describe(
"Optional directory inside targetPath. Omit it or use '.' for the whole target.",
),
targetSummary: z
.string()
.trim()
targetSummary: trimmedText
.max(2400)
.optional()
.describe("Optional bounded target context."),
userContext: editableUserContextSchema
userContext: trimmedText
.optional()
.describe("Optional security focus supplied by the user."),
};
type PromptOnlyScanInput = {
diffTarget?: z.output<typeof diffTargetSchema>;
mode: "diff" | "standard";
scope: string;
targetPath: string;
targetSummary?: string;
userContext?: string;
};
type PromptOnlyScanInput = z.output<
z.ZodObject<typeof startPromptOnlyScanSchema>
>;
const userInputOptionSchema = z.strictObject({
description: z.string().trim().min(1).max(1200),
label: z.string().trim().min(1).max(200),
description: requiredText.max(1200),
label: requiredText.max(200),
});
const userInputQuestionSchema = z
.strictObject({
header: z.string().trim().min(1).max(64),
header: requiredText.max(64),
id: z
.string()
.regex(
Expand All @@ -224,7 +209,7 @@ const userInputQuestionSchema = z
"Question IDs must not use reserved object property names.",
),
options: z.array(userInputOptionSchema).min(2).max(3),
question: z.string().trim().min(1).max(1200),
question: requiredText.max(1200),
})
.superRefine((question, context) => {
const labels = new Set<string>();
Expand Down Expand Up @@ -270,8 +255,8 @@ const submissionSchema = {
scope: pathSchema,
sessionId: z.string().uuid(),
targetPath: pathSchema,
targetSummary: z.string().trim().max(2400).optional(),
userContext: editableUserContextSchema.optional(),
targetSummary: trimmedText.max(2400).optional(),
userContext: trimmedText.optional(),
};
const setupInspectionSchema = {
diffTarget: diffTargetSchema.optional(),
Expand All @@ -296,7 +281,7 @@ const startDeepScanSchema = {
.describe(
"Scope inside targetPath. Deep Scan currently requires the whole target.",
),
userContext: editableUserContextSchema
userContext: trimmedText
.optional()
.describe("Optional security focus supplied by the user."),
handoffClaimToken: handoffClaimTokenSchema
Expand All @@ -315,13 +300,13 @@ const continuationMutationClaimSchema = {
const scanContextUpdateSchema = {
...scanSchema,
...continuationMutationClaimSchema,
userContext: editableUserContextSchema.describe(
userContext: trimmedText.describe(
"Complete replacement context for the running scan. Pass an empty string to clear it.",
),
};
const appScanContextUpdateSchema = {
...scanSchema,
userContext: editableUserContextSchema,
userContext: trimmedText,
};
const phaseProgressUnitSchema = z.enum([
"checks",
Expand All @@ -332,10 +317,7 @@ const phaseProgressUnitSchema = z.enum([
"report_artifacts",
]);
const progressSchema = {
deepReviewPass: z
.number()
.int()
.positive()
deepReviewPass: positiveInteger
.optional()
.describe(
"Current Deep Scan discovery pass. Send it when starting each pass together with that pass's total and zero completed items.",
Expand All @@ -353,18 +335,12 @@ const progressSchema = {
.describe(
"Current workflow phase. Send it immediately when the scan enters a new phase so persisted progress advances.",
),
phaseItemsCompleted: z
.number()
.int()
.nonnegative()
phaseItemsCompleted: nonnegativeInteger
.optional()
.describe(
"Completed authoritative coverage, receipts, or artifacts for the current phase. Increase it only after the corresponding work product exists.",
),
phaseItemsTotal: z
.number()
.int()
.nonnegative()
phaseItemsTotal: nonnegativeInteger
.optional()
.describe(
"Expected authoritative coverage, receipts, or artifacts for the current phase. Increase it before newly discovered work begins.",
Expand All @@ -381,19 +357,13 @@ const progressSchema = {
.describe(
"Current standard or diff scan preflight results. Project every helper results entry to capability, reason, severity, and status only. The server derives the item counts and visible block/warn attention items.",
),
reportableFindingsCount: z.number().int().nonnegative().optional(),
reviewItemsCompleted: z
.number()
.int()
.nonnegative()
reportableFindingsCount: nonnegativeInteger.optional(),
reviewItemsCompleted: nonnegativeInteger
.optional()
.describe(
"Cumulative completed reviews or coverage surfaces in the current discovery pass. Increment only after the corresponding review is complete.",
),
reviewItemsTotal: z
.number()
.int()
.nonnegative()
reviewItemsTotal: nonnegativeInteger
.optional()
.describe(
"Expected reviews or coverage surfaces in the current discovery pass. Increase it before assigning newly discovered work.",
Expand All @@ -403,14 +373,14 @@ const progressSchema = {
};
const failSchema = {
...continuationMutationClaimSchema,
message: z.string().trim().min(1).max(2400),
message: requiredText.max(2400),
scanId: z.string().uuid(),
};
const completeScanSchema = {
...scanSchema,
...continuationMutationClaimSchema,
};
const occurrenceIdSchema = z.string().trim().min(1).max(256);
const occurrenceIdSchema = requiredText.max(256);
const scanReadSchema = {
...scanSchema,
occurrenceId: occurrenceIdSchema
Expand All @@ -431,14 +401,14 @@ const findingTriageSchema = {
closeReason: z
.enum(["already_fixed", "wont_fix", "false_positive"])
.optional(),
note: z.string().trim().max(2400).optional(),
note: trimmedText.max(2400).optional(),
occurrenceId: occurrenceIdSchema,
status: z.enum(["open", "closed"]),
};
const findingRemediationSchema = {
actionToken: z.string().uuid(),
baseRevision: z.string().trim().min(1).max(512).optional(),
expectedVersion: z.number().int().positive(),
baseRevision: requiredText.max(512).optional(),
expectedVersion: positiveInteger,
occurrenceId: occurrenceIdSchema,
patchDigest: z
.string()
Expand All @@ -447,13 +417,13 @@ const findingRemediationSchema = {
patchPath: pathSchema.optional(),
requestId: z.string().uuid(),
state: z.enum(["generated", "applied", "verifying", "verified", "failed"]),
summary: z.string().trim().max(2400).optional(),
verificationSummary: z.string().trim().max(2400).optional(),
summary: trimmedText.max(2400).optional(),
verificationSummary: trimmedText.max(2400).optional(),
};
const findingRemediationActionRequestSchema = {
action: z.enum(["apply", "verify"]),
actionToken: z.string().uuid(),
expectedVersion: z.number().int().positive(),
expectedVersion: positiveInteger,
occurrenceId: occurrenceIdSchema,
requestId: z.string().uuid(),
};
Expand All @@ -468,19 +438,19 @@ const findingsExportSchema = {
scanId: z.string().uuid(),
};
const collectionPageSchema = {
limit: z.number().int().positive().max(50).optional(),
offset: z.number().int().nonnegative().optional(),
limit: positiveInteger.max(50).optional(),
offset: nonnegativeInteger.optional(),
};
const findingCollectionFiltersSchema = {
query: z.string().trim().max(512).optional(),
query: trimmedText.max(512).optional(),
severity: z
.enum(["critical", "high", "medium", "low", "informational"])
.optional(),
status: z.enum(["open", "closed"]).optional(),
};
const targetCollectionFiltersSchema = {
query: z.string().trim().max(512).optional(),
targetId: z.string().trim().min(1).max(256).optional(),
query: trimmedText.max(512).optional(),
targetId: requiredText.max(256).optional(),
};
const findingsPageSchema = {
...collectionPageSchema,
Expand All @@ -489,9 +459,9 @@ const findingsPageSchema = {
};
const globalFindingsPageSchema = {
...findingCollectionFiltersSchema,
limit: z.number().int().positive().max(20).optional(),
offset: z.number().int().nonnegative().optional(),
targetId: z.string().trim().min(1).max(256).optional(),
limit: positiveInteger.max(20).optional(),
offset: nonnegativeInteger.optional(),
targetId: requiredText.max(256).optional(),
};
const scanListSchema = {
...collectionPageSchema,
Expand Down
33 changes: 9 additions & 24 deletions plugins/codex-security/mcp-app/src/artifact-io.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { canonicalDirectory } from "./artifact-context.js";
import { isRecord } from "./record.js";
import type { ZodType, ZodError } from "zod/v4";
import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import { dirname, isAbsolute, join, sep } from "node:path";
Expand Down Expand Up @@ -41,17 +41,6 @@ export interface ArtifactPage {
limit?: number;
}

export interface ArtifactPageResult<Row> {
rows: Row[];
nextCursor?: string;
}

export interface ArtifactRowSchema<Row> {
safeParse(
value: unknown,
): { success: true; data: Row } | { success: false; error?: unknown };
}

/**
* Components are fixed, internal operation constants, not model-facing paths.
*/
Expand Down Expand Up @@ -148,7 +137,7 @@ export async function readArtifactJsonl<Row>(
context: ArtifactContext,
components: readonly string[],
label: string,
rowSchema: ArtifactRowSchema<Row>,
rowSchema: ZodType<Row>,
): Promise<Row[]> {
const source = await readArtifactText(context, components, label);
const rows: Row[] = [];
Expand Down Expand Up @@ -185,7 +174,7 @@ export function paginateArtifactRows<Row>(
rows: readonly Row[],
page: ArtifactPage,
label: string,
): ArtifactPageResult<Row> {
) {
// The MCP tool schemas validate cursor syntax and limit bounds.
const start = Number(page.cursor ?? "0");
if (!Number.isSafeInteger(start) || start > rows.length) {
Expand Down Expand Up @@ -328,16 +317,12 @@ async function inspectOptionalPath(
}
}

function formatRowSchemaError(error: unknown): string {
if (!isRecord(error) || !Array.isArray(error.issues)) return ".";
const issue: unknown = error.issues[0];
if (!isRecord(issue)) return ".";
const issuePath = Array.isArray(issue.path)
? issue.path.map(String).join(".")
: "";
const message = typeof issue.message === "string" ? issue.message : "";
if (!issuePath && !message) return ".";
return ": " + (issuePath ? issuePath + ": " : "") + message;
function formatRowSchemaError(error: ZodError): string {
const issue = error.issues[0];
if (!issue) return ".";
const issuePath = issue.path.map(String).join(".");
if (!issuePath && !issue.message) return ".";
return ": " + (issuePath ? issuePath + ": " : "") + issue.message;
}

function isNodeError(error: unknown): error is NodeJS.ErrnoException {
Expand Down
Loading
Loading