fix(canvas): cap diff canvas size in schema (codex pass 13 P2)

Agent-emitted diff canvases had no line/hunk/file caps in the Zod
schema. With the existing 256 KB byte budget per canvas an attacker
or buggy agent could pack tens of thousands of one-character add/del
lines, causing CanvasDiff to render hundreds of thousands of DOM rows
on every 5s poll and freeze the session detail page.

Adds three caps in canvas-schema.ts mirroring the synthesized git-diff
path's existing constants:

  DIFF_MAX_LINES_PER_HUNK = 1000
  DIFF_MAX_HUNKS_PER_FILE = 50
  DIFF_MAX_FILES_PER_CANVAS = 200

Two new tests prove over-cap diffs are rejected.

Refs: docs/canvases-codex-review-2026-05-06.html (the P2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ashish Huddar 2026-05-09 22:32:59 +05:30
parent 0b8c245fae
commit 89cf367e5f
2 changed files with 50 additions and 3 deletions

View File

@ -134,6 +134,44 @@ describe("CanvasArtifactSchema", () => {
const result = CanvasArtifactSchema.safeParse({ ...validMarkdown, id: "BAD ID" });
expect(result.success).toBe(false);
});
it("rejects diff canvases with too many hunk lines", () => {
// 1001 lines exceeds DIFF_MAX_LINES_PER_HUNK (1000). This is the cap that
// prevents agent-emitted diffs from rendering hundreds of thousands of DOM
// rows on every 5s poll.
const tooManyLines = Array.from({ length: 1001 }, (_, i) => ({
kind: "context" as const,
text: `line ${i}`,
}));
const result = CanvasArtifactSchema.safeParse({
version: 1,
id: "huge",
type: "diff",
title: "Too many lines",
createdAt: "2026-05-06T00:00:00Z",
updatedAt: "2026-05-06T00:00:00Z",
payload: { files: [{ path: "a.ts", status: "modified", hunks: [{ header: "@@", lines: tooManyLines }] }] },
});
expect(result.success).toBe(false);
});
it("rejects diff canvases with too many files", () => {
const tooManyFiles = Array.from({ length: 201 }, (_, i) => ({
path: `f${i}.ts`,
status: "modified" as const,
hunks: [],
}));
const result = CanvasArtifactSchema.safeParse({
version: 1,
id: "wide",
type: "diff",
title: "Too many files",
createdAt: "2026-05-06T00:00:00Z",
updatedAt: "2026-05-06T00:00:00Z",
payload: { files: tooManyFiles },
});
expect(result.success).toBe(false);
});
});
describe("parseUnifiedDiff", () => {

View File

@ -24,10 +24,19 @@ const diffLine = z
})
.strict();
// Bounds matching the synthesized git-diff path's caps in canvas-log.ts.
// Without these, an agent-emitted diff canvas could pack tens of thousands of
// hunk lines into a 256 KB payload and stall the session detail page on every
// 5s poll. Numbers chosen to be generous for legitimate review/refactor diffs
// while preventing DOM blowup. See canvas-log.ts: DIFF_MAX_FILES, DIFF_MAX_LINES.
const DIFF_MAX_LINES_PER_HUNK = 1000;
const DIFF_MAX_HUNKS_PER_FILE = 50;
const DIFF_MAX_FILES_PER_CANVAS = 200;
const diffHunk = z
.object({
header: z.string(),
lines: z.array(diffLine),
lines: z.array(diffLine).max(DIFF_MAX_LINES_PER_HUNK),
})
.strict();
@ -36,13 +45,13 @@ const diffFile = z
path: z.string().min(1),
oldPath: z.string().optional(),
status: z.enum(["added", "modified", "deleted", "renamed"]),
hunks: z.array(diffHunk),
hunks: z.array(diffHunk).max(DIFF_MAX_HUNKS_PER_FILE),
})
.strict();
const diffPayload = z
.object({
files: z.array(diffFile),
files: z.array(diffFile).max(DIFF_MAX_FILES_PER_CANVAS),
})
.strict();