Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377import { createHash } from "node:crypto";import { parseSkillFrontmatter, type SkillManifestEntry } from "agents/skills";import { z } from "zod";import { skillRequirements } from "../shared/skill-permissions";import { SKILL_FORMAT_VERSION, SKILL_MAX_FILE_BYTES, SKILL_MAX_PACKAGE_BYTES, SKILL_MAX_INSTRUCTION_BYTES, skillName, skillVersion, skillOrigin, skillPackageInput, type SkillPackageFile, type SkillPackageMetadata, type SkillOrigin,} from "../shared/skills";
export class SkillPackageError extends Error { constructor( readonly code: string, readonly path: string, message: string, ) { super(message); this.name = "SkillPackageError"; }}const fail = (code: string, path: string, message: string): never => { throw new SkillPackageError(code, path, message);};function schemaFailure(error: z.ZodError, code: string, prefix: string) { const issue = error.issues[0]; const parts = [...issue.path]; if (issue.code === "unrecognized_keys") parts.push(issue.keys[0]); const path = [prefix, ...parts.map((part) => String(part).slice(0, 80))] .filter(Boolean) .join(".") .slice(0, 240); const message = issue.code === "unrecognized_keys" ? "Remove this unsupported field" : issue.code === "too_big" ? `Use at most ${issue.maximum} ${issue.origin === "string" ? "characters" : "entries"}` : issue.code === "too_small" ? `Provide at least ${issue.minimum} ${issue.origin === "string" ? "characters" : "entries"}` : issue.message.slice(0, 240); return fail(code, path, message);}const textTypes = new Map([ ["md", "text/markdown"], ["txt", "text/plain"], ["json", "application/json"], ["csv", "text/csv"], ["yaml", "application/yaml"], ["yml", "application/yaml"], ["svg", "image/svg+xml"],]);const binaryTypes = new Map([ ["png", "image/png"], ["jpg", "image/jpeg"], ["jpeg", "image/jpeg"], ["gif", "image/gif"], ["webp", "image/webp"], ["pdf", "application/pdf"],]);const scriptTypes = new Map([ ["js", "text/javascript"], ["mjs", "text/javascript"], ["py", "text/x-python"], ["sh", "text/x-shellscript"], ["bash", "text/x-shellscript"],]);const metadataSchema = z.strictObject({ name: skillName, description: z.string().trim().min(1).max(1024), metadata: z .record(z.string().min(1).max(64), z.string().max(1024)) .refine((value) => Object.keys(value).length <= 32) .refine( (value) => skillVersion.safeParse(value.version).success, "metadata.version must be a semantic version, such as 1.0.0", ), license: z.string().max(200).optional(), compatibility: z.string().max(500).optional(), "allowed-tools": z.string().max(2000).optional(),});
export interface ValidatedSkillPackage extends SkillPackageMetadata { files: SkillPackageFile[]; entry: SkillManifestEntry & { metadata?: Record<string, string> };}export function validateSkillPath(path: string, field: string) { if ( !path .split("/") .every( (part) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(part) && !part.endsWith(".") && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part), ) ) fail( "invalid_path", field, "Use a relative slash-separated path without traversal, escapes, hidden files, trailing dots or reserved device names", ); if ( path !== "SKILL.md" && path !== "permissions.json" && !/\.md$/i.test(path) && !/^(resources|references|assets|scripts)\//.test(path) ) fail( "invalid_path", field, "Markdown files may use any safe relative path; place other files under resources/, references/, assets/ or scripts/", );}function contentBytes(file: SkillPackageFile, field: string) { let byteLength: number; if (file.encoding === "base64") { try { const decoded = atob(file.content); if (btoa(decoded) !== file.content) throw new Error(); byteLength = decoded.length; } catch { return fail( "invalid_encoding", field, "Use canonical padded base64 without whitespace", ); } } else { if (file.content.includes("\0")) return fail( "invalid_encoding", field, "Text files cannot contain NUL bytes", ); const bytes = new TextEncoder().encode(file.content); if ( new TextDecoder("utf-8", { ignoreBOM: true, fatal: true }).decode( bytes, ) !== file.content ) return fail( "invalid_encoding", field, "Text must contain valid Unicode without unpaired surrogates", ); byteLength = bytes.byteLength; } if (byteLength > SKILL_MAX_FILE_BYTES) return fail( "file_too_large", field, "Each file must be at most 256 KiB after decoding", ); return byteLength;}
// Origin and reserved names come from the installer, separately from untrusted// package bytes. Validation never executes scripts or grants allowed-tools hints.export function validateSkillPackage( value: unknown, origin: SkillOrigin, reservedNames: ReadonlySet<string> = new Set(),): ValidatedSkillPackage { const parsed = skillPackageInput.safeParse(value); if (!parsed.success) return schemaFailure(parsed.error, "invalid_package", ""); const parsedOrigin = skillOrigin.safeParse(origin); if (!parsedOrigin.success) return fail( "invalid_origin", "origin", "Provide a bounded bundled release, upload filename or HTTPS source without credentials", ); const provenance = parsedOrigin.data; const indexedFiles = parsed.data.files.map((file, index) => ({ file, index, })); indexedFiles.sort((a, b) => a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0, ); const files = indexedFiles.map(({ file }) => file); const identifiers = new Set<string>(); const resources: NonNullable<SkillManifestEntry["resources"]> = []; let totalBytes = 0; const digest = createHash("sha256").update( JSON.stringify([SKILL_FORMAT_VERSION]), ); for (const { index, file } of indexedFiles) { const field = `files.${index}`; validateSkillPath(file.path, `${field}.path`); const key = file.path.toLowerCase(); if (identifiers.has(key)) fail( "duplicate_path", `${field}.path`, "Each file path must be unique, including case variants", ); if ( [...identifiers].some( (existing) => existing.startsWith(`${key}/`) || key.startsWith(`${existing}/`), ) ) fail( "duplicate_path", `${field}.path`, "A path cannot be both a file and a directory", ); identifiers.add(key); const byteLength = contentBytes(file, `${field}.content`); totalBytes += byteLength; if (totalBytes > SKILL_MAX_PACKAGE_BYTES) fail( "package_too_large", "files", "Package contents must total at most 2 MiB after decoding", ); digest.update(JSON.stringify([file.path, file.encoding, file.content])); if (file.path === "SKILL.md") { if (file.encoding !== "text") fail( "invalid_encoding", `${field}.encoding`, "SKILL.md must use text encoding", ); if (byteLength > 64 * 1024) fail( "file_too_large", `${field}.content`, "SKILL.md must be at most 64 KiB", ); continue; } const rawExtension = file.path.split(".").at(-1)!; const extension = /^md$/i.test(rawExtension) ? "md" : rawExtension; const script = file.path.startsWith("scripts/") && extension !== "md"; const mimeType = ( script ? scriptTypes : file.encoding === "base64" ? binaryTypes : textTypes ).get(extension); if (!mimeType || (script && file.encoding !== "text")) fail( "unsupported_file", `${field}.path`, "Use supported text/image/PDF resources or text .js, .mjs, .py, .sh and .bash scripts; compile TypeScript before packaging", ); resources.push({ path: file.path, encoding: file.encoding, content: file.content, size: byteLength, mimeType, kind: script ? "script" : file.path.startsWith("references/") ? "reference" : file.path.startsWith("assets/") ? "asset" : "file", }); } const entry = files.find((file) => file.path === "SKILL.md"); if (!entry) return fail( "missing_instructions", "files", "Add a root SKILL.md with frontmatter and entry instructions", ); let frontmatter: ReturnType<typeof parseSkillFrontmatter>; try { frontmatter = parseSkillFrontmatter(entry.content); } catch { return fail( "invalid_metadata", "SKILL.md", "Fix the YAML frontmatter; duplicate keys, malformed YAML and excessive aliases are not supported", ); } if ( frontmatter.data.metadata !== null && typeof frontmatter.data.metadata === "object" && Object.hasOwn(frontmatter.data.metadata, "__proto__") ) return fail( "invalid_metadata", "SKILL.md.metadata.__proto__", "Remove the reserved __proto__ metadata key", ); const metadata = metadataSchema.safeParse(frontmatter.data); if (!metadata.success) return schemaFailure(metadata.error, "invalid_metadata", "SKILL.md"); if (reservedNames.has(metadata.data.name)) return fail( "duplicate_skill", "SKILL.md.name", "This Skill name is already registered; choose a new name or use an explicit update flow", ); if ( !frontmatter.body.trim() || new TextEncoder().encode(frontmatter.body).byteLength > SKILL_MAX_INSTRUCTION_BYTES ) return fail( "invalid_instructions", "SKILL.md", "Provide nonempty entry instructions of at most 32 KiB", ); const declaration = files.find((file) => file.path === "permissions.json"); let requirements: unknown = {}; if (declaration) { if ( declaration.encoding !== "text" || new TextEncoder().encode(declaration.content).byteLength > 8192 ) fail( "invalid_permissions", "permissions.json", "Use a text permission declaration of at most 8 KiB", ); try { requirements = JSON.parse(declaration.content); } catch { fail( "invalid_permissions", "permissions.json", "Use valid JSON for permission requirements", ); } } const permissions = skillRequirements.safeParse(requirements); if (!permissions.success) return schemaFailure( permissions.error, "invalid_permissions", "permissions.json", ); const native: ValidatedSkillPackage["entry"] = { name: metadata.data.name, description: metadata.data.description, metadata: metadata.data.metadata, license: metadata.data.license?.trim() || undefined, compatibility: metadata.data.compatibility?.trim() || undefined, allowedTools: metadata.data["allowed-tools"]?.trim() || undefined, body: frontmatter.body, version: metadata.data.metadata.version, rawContent: entry.content, resources, }; return { formatVersion: SKILL_FORMAT_VERSION, name: native.name, description: native.description, version: metadata.data.metadata.version, fingerprint: digest.digest("hex"), origin: provenance, fileCount: files.length, totalBytes, scripts: resources .filter((resource) => resource.kind === "script") .map((resource) => resource.path), permissions: permissions.data, files, entry: native, };}