diff --git a/src/main/skill-github.ts b/src/main/skill-github.ts index 6345e750..ab2feb39 100644 --- a/src/main/skill-github.ts +++ b/src/main/skill-github.ts @@ -15,8 +15,11 @@ interface Logger { error: (...args: unknown[]) => void; } -/** 1 MB — generous upper bound for a single SKILL.md file. */ -const MAX_SKILL_BYTES = 1024 * 1024; +/** 2 MB — generous upper bound for a single SKILL.md (markdown) file. */ +export const MAX_SKILL_BYTES = 2 * 1024 * 1024; + +/** 32 MB — aggregate upper bound for all SKILL.md content from a single skill pack. */ +export const MAX_TOTAL_BYTES = 32 * 1024 * 1024; /** 10 MB — upper bound for the recursive git tree JSON response. */ const MAX_TREE_BYTES = 10 * 1024 * 1024; @@ -149,6 +152,7 @@ export async function fetchGitHubSkills( } const skills: GitHubSkillData[] = []; + let totalBytes = 0; for (const skillMdPath of skillMdPaths) { try { @@ -159,7 +163,20 @@ export async function fetchGitHubSkills( logger?.warn(`Failed to download ${rawUrl}: ${contentResponse.status}`); continue; } - const markdown = await readResponseWithLimit(contentResponse, MAX_SKILL_BYTES); + const remainingBudget = MAX_TOTAL_BYTES - totalBytes; + const perFileCap = Math.min(MAX_SKILL_BYTES, Math.max(0, remainingBudget)); + if (perFileCap === 0) { + throw new Error( + `Skill pack exceeded aggregate limit of ${MAX_TOTAL_BYTES} bytes`, + ); + } + const markdown = await readResponseWithLimit(contentResponse, perFileCap); + totalBytes += Buffer.byteLength(markdown, "utf-8"); + if (totalBytes > MAX_TOTAL_BYTES) { + throw new Error( + `Skill pack exceeded aggregate limit of ${MAX_TOTAL_BYTES} bytes`, + ); + } // Collect sibling files in the same directory (or subdirectories) const skillDir = diff --git a/src/main/skills/import-github.ts b/src/main/skills/import-github.ts index 80b1f5f7..0049d1f9 100644 --- a/src/main/skills/import-github.ts +++ b/src/main/skills/import-github.ts @@ -15,6 +15,8 @@ import { import { resolveGitHubCommitSha, fetchGitHubSkills, + MAX_SKILL_BYTES, + MAX_TOTAL_BYTES, } from "../skill-github.js"; interface GitHubImportDeps { @@ -49,6 +51,11 @@ export function createGitHubImporter({ db, logger, skillFilesDir }: GitHubImport markdown: string, _inventory: FileInventoryEntry[], ): Promise { + if (markdown.length * 2 > MAX_SKILL_BYTES) { + throw new Error( + `SKILL.md exceeds ${MAX_SKILL_BYTES}-byte limit`, + ); + } const dir = skillFilesDir(companyId, slug); await mkdir(dir, { recursive: true }); await writeFile(join(dir, "SKILL.md"), markdown, "utf-8"); @@ -83,8 +90,16 @@ export function createGitHubImporter({ db, logger, skillFilesDir }: GitHubImport const results: SkillImportResult[] = []; const sourceType: SkillSourceType = originalSkillsShUrl ? "skills_sh" : "github"; const nowTs = new Date().toISOString(); + let totalMaterializedBytes = 0; for (const ghSkill of githubSkills) { + const skillBytes = Buffer.byteLength(ghSkill.markdown, "utf-8"); + totalMaterializedBytes += skillBytes; + if (totalMaterializedBytes > MAX_TOTAL_BYTES) { + throw new Error( + `Skill pack exceeded aggregate limit of ${MAX_TOTAL_BYTES} bytes`, + ); + } const parsed = parseFrontmatterMarkdown(ghSkill.markdown); const rawSlug = normalizeSkillSlug(parsed.frontmatter.slug as string | undefined) ?? diff --git a/src/main/skills/import.ts b/src/main/skills/import.ts index d492574e..5d33a84b 100644 --- a/src/main/skills/import.ts +++ b/src/main/skills/import.ts @@ -17,6 +17,7 @@ import { parseSkillImportSource, } from "../skill-keys.js"; import { createGitHubImporter } from "./import-github"; +import { MAX_SKILL_BYTES } from "../skill-github.js"; const BLOCKED_PATH_PREFIXES_UNIX = ["/etc", "/var", "/proc", "/sys", "/dev", "/boot", "/sbin", "/usr/sbin"]; const BLOCKED_PATH_PREFIXES_WIN = ["C:\\Windows", "C:\\Program Files", "C:\\ProgramData"]; @@ -63,6 +64,11 @@ export function createSkillImporter({ db, profileDir: _profileDir, logger, skill markdown: string, _inventory: FileInventoryEntry[], ): Promise { + if (markdown.length * 2 > MAX_SKILL_BYTES) { + throw new Error( + `SKILL.md exceeds ${MAX_SKILL_BYTES}-byte limit`, + ); + } const dir = skillFilesDir(companyId, slug); await mkdir(dir, { recursive: true }); await writeFile(join(dir, "SKILL.md"), markdown, "utf-8");