UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

155 lines 6.84 kB
/** * Bundle Manifest Schema (Zod) * * Validates `.aiwg/{extensions,addons,frameworks,plugins}/<name>/manifest.json` * for project-local artifact bundles. Implements the design in * `.aiwg/architecture/design-manifest-schema.md` (#1044). * * Layered above the per-artifact Extension type validation in `validation.ts`: * the bundle manifest declares the bundle (a directory containing artifacts); * the existing schemas validate individual artifact bodies inside the bundle. * * @implements #1044 * @architecture .aiwg/architecture/design-manifest-schema.md */ import { z } from 'zod'; import { PlatformCompatibilitySchema, DeploymentConfigSchema, MemoryFootprintSchema, DeprecationSchema, } from './validation.js'; // ============================================ // Limits (from #1042 threat model + #1044 design) // ============================================ export const MANIFEST_MAX_BYTES = 64 * 1024; // 64 KB export const MAX_BUNDLES_PER_PROJECT = 200; export const MAX_KEYWORDS_PER_MANIFEST = 50; export const MAX_OVERRIDES_PER_MANIFEST = 20; // ============================================ // Project-local bundle types (#1040) // ============================================ export const ProjectLocalTypeSchema = z.enum([ 'extension', 'addon', 'framework', 'plugin', ]); // ============================================ // Path safety helpers // ============================================ /** * Relative paths within a bundle. No `..`, no leading `/`, alphanumeric + * underscore + hyphen, optional trailing slash. */ const safeRelativePath = z.string().regex(/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*\/?$/, 'must be a relative path (alphanumeric + _-, no leading slash, no ..)'); // Single-char ids are allowed; multi-char ids must end with alphanumeric // (no trailing hyphen). This pattern: `[a-z0-9]([a-z0-9-]*[a-z0-9])?` const bundleNamePattern = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/; // ============================================ // Per-type config blocks // ============================================ const ArtifactListsSchema = z.object({ agents: z.array(z.string()).max(200).optional(), skills: z.array(z.string()).max(500).optional(), rules: z.array(z.string()).max(200).optional(), templates: z.array(z.string()).max(200).optional(), prompts: z.array(z.string()).max(200).optional(), hooks: z.array(z.string()).max(50).optional(), commands: z.array(z.string()).max(200).optional(), behaviors: z.array(z.string()).max(50).optional(), }); const EntryPathsSchema = z.object({ agents: safeRelativePath.optional(), skills: safeRelativePath.optional(), rules: safeRelativePath.optional(), templates: safeRelativePath.optional(), prompts: safeRelativePath.optional(), hooks: safeRelativePath.optional(), commands: safeRelativePath.optional(), behaviors: safeRelativePath.optional(), }).strict(); export const AddonConfigSchema = z.object({ entry: EntryPathsSchema.optional(), ...ArtifactListsSchema.shape, core: z.boolean().optional(), autoInstall: z.boolean().optional(), }).strict(); export const FrameworkConfigSchema = z.object({ path: safeRelativePath.optional(), files: z.array(safeRelativePath).max(100).optional(), ignore: z.array(safeRelativePath).max(100).optional(), contextContributions: z.object({ hookFragment: safeRelativePath.optional(), sectionsDir: safeRelativePath.optional(), sectionsManifest: safeRelativePath.optional(), priority: z.number().int().min(0).max(100).optional(), description: z.string().max(512).optional(), }).strict().optional(), }).strict(); export const ExtensionConfigSchema = z.object({ entry: EntryPathsSchema.optional(), ...ArtifactListsSchema.shape, }).strict(); export const PluginConfigSchema = z.object({ payloadType: z.enum(['addon', 'framework', 'extension']), payloadPath: safeRelativePath, }).strict(); // ============================================ // Top-level BundleManifestSchema // ============================================ export const BundleManifestSchema = z.object({ // Required core fields id: z.string() .min(1) .max(64) .regex(bundleNamePattern, 'kebab-case alphanumeric, no leading/trailing hyphen'), type: ProjectLocalTypeSchema, name: z.string().min(1).max(128), version: z.string().regex(/^\d+\.\d+\.\d+/, 'CalVer or SemVer (X.Y.Z[...])'), description: z.string().min(1).max(1024), manifestVersion: z.literal('1'), platforms: PlatformCompatibilitySchema, keywords: z.array(z.string().max(64)).min(1).max(MAX_KEYWORDS_PER_MANIFEST), deployment: DeploymentConfigSchema, // Optional standard metadata author: z.string().max(128).optional(), license: z.string().max(64).optional(), repository: z.string().url().max(512).optional(), // Type-discriminated nested config (exactly one matches `type`) addonConfig: AddonConfigSchema.optional(), frameworkConfig: FrameworkConfigSchema.optional(), extensionConfig: ExtensionConfigSchema.optional(), pluginConfig: PluginConfigSchema.optional(), // Override / safety-critical declarations (#1041) 'safety-critical': z.boolean().optional(), overrides: z.array(z.string().max(64)).max(MAX_OVERRIDES_PER_MANIFEST).optional(), // Optional patterns shared with existing extension validation deprecation: DeprecationSchema.optional(), memory: MemoryFootprintSchema.optional(), }) .strict() .refine((m) => { // Discriminator alignment: type must match the present *Config block(s) const present = { addon: m.addonConfig !== undefined, framework: m.frameworkConfig !== undefined, extension: m.extensionConfig !== undefined, plugin: m.pluginConfig !== undefined, }; const presentCount = Object.values(present).filter(Boolean).length; // Extension is the only type that may omit its config block; others require it if (m.type === 'extension') { return presentCount === 0 || (presentCount === 1 && present.extension); } return presentCount === 1 && present[m.type]; }, { message: 'type must match its corresponding *Config block (addon→addonConfig, framework→frameworkConfig, plugin→pluginConfig; extension may omit extensionConfig)' }); /** * Convert a Zod error into the structured ManifestValidationError shape used * by discovery and `aiwg validate-metadata`. */ export function zodErrorToValidationErrors(err, manifestPath) { return err.errors.map((issue) => ({ path: manifestPath, field: issue.path.join('.') || '(root)', expected: issue.message, actual: 'received' in issue ? String(issue.received) : 'invalid', severity: 'error', })); } //# sourceMappingURL=manifest.js.map