Add managed plugin skill contracts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-07 09:31:58 -05:00
1 parent d0e9cc76f2
commit 268673e84c
17 files changed
+918 -24

No files matched your search

+60 -1
View File
@@ -151,6 +151,7 @@ export const pluginManagedAgentDeclarationSchema = z.object({
instructions: z.object({
entryFile: z.string().min(1).max(200).optional(),
content: z.string().max(200_000).optional(),
files: z.record(z.string().max(200_000)).optional(),
assetPath: z.string().min(1).max(500).optional(),
}).optional(),
});
@@ -172,7 +173,7 @@ export type PluginManagedProjectDeclarationInput = z.infer<typeof pluginManagedP
const pluginManagedResourceRefSchema = z.object({
pluginKey: z.string().min(1).max(100).optional(),
resourceKind: z.enum(["agent", "project", "routine"]),
resourceKind: z.enum(["agent", "project", "routine", "skill"]),
resourceKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
message: "resourceKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
}),
@@ -232,6 +233,41 @@ export const pluginLocalFolderDeclarationSchema = z.object({
export type PluginLocalFolderDeclarationInput = z.infer<typeof pluginLocalFolderDeclarationSchema>;
export const pluginManagedSkillFileDeclarationSchema = z.object({
path: pluginLocalFolderRelativePathSchema.refine(
(value) => value.toLowerCase() !== "skill.md",
{ message: "managed skill files cannot replace SKILL.md; use markdown for the main skill file" },
),
content: z.string().max(200_000),
});
export type PluginManagedSkillFileDeclarationInput = z.infer<typeof pluginManagedSkillFileDeclarationSchema>;
export const pluginManagedSkillDeclarationSchema = z.object({
skillKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
message: "skillKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
}),
displayName: z.string().min(1).max(100),
slug: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
message: "slug must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
}).optional(),
description: z.string().max(2000).nullable().optional(),
markdown: z.string().max(200_000).optional(),
files: z.array(pluginManagedSkillFileDeclarationSchema).max(50).optional(),
}).superRefine((value, ctx) => {
const paths = (value.files ?? []).map((file) => file.path);
const duplicates = paths.filter((path, index) => paths.indexOf(path) !== index);
if (duplicates.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate managed skill file paths: ${[...new Set(duplicates)].join(", ")}`,
path: ["files"],
});
}
});
export type PluginManagedSkillDeclarationInput = z.infer<typeof pluginManagedSkillDeclarationSchema>;
/**
* Validates a {@link PluginUiSlotDeclaration} — a UI extension slot the plugin
* fills with a React component. Includes `superRefine` checks for slot-specific
@@ -589,6 +625,7 @@ export const pluginManifestV1Schema = z.object({
agents: z.array(pluginManagedAgentDeclarationSchema).optional(),
projects: z.array(pluginManagedProjectDeclarationSchema).optional(),
routines: z.array(pluginManagedRoutineDeclarationSchema).optional(),
skills: z.array(pluginManagedSkillDeclarationSchema).optional(),
localFolders: z.array(pluginLocalFolderDeclarationSchema).optional(),
launchers: z.array(pluginLauncherDeclarationSchema).optional(),
ui: z.object({
@@ -678,6 +715,16 @@ export const pluginManifestV1Schema = z.object({
}
}
if (manifest.skills && manifest.skills.length > 0) {
if (!manifest.capabilities.includes("skills.managed")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Capability 'skills.managed' is required when managed skills are declared",
path: ["capabilities"],
});
}
}
if (manifest.localFolders && manifest.localFolders.length > 0) {
if (!manifest.capabilities.includes("local.folders")) {
ctx.addIssue({
@@ -871,6 +918,18 @@ export const pluginManifestV1Schema = z.object({
}
}
if (manifest.skills) {
const skillKeys = manifest.skills.map((skill) => skill.skillKey);
const duplicates = skillKeys.filter((key, i) => skillKeys.indexOf(key) !== i);
if (duplicates.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate managed skill keys: ${[...new Set(duplicates)].join(", ")}`,
path: ["skills"],
});
}
}
// UI slot ids must be unique within the plugin (namespaced at runtime)
if (manifest.ui) {
if (manifest.ui.slots) {