@@ -0,0 +1,670 @@
import { z } from "zod" ;
import {
PLUGIN_STATUSES ,
PLUGIN_CATEGORIES ,
PLUGIN_CAPABILITIES ,
PLUGIN_UI_SLOT_TYPES ,
PLUGIN_UI_SLOT_ENTITY_TYPES ,
PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS ,
PLUGIN_LAUNCHER_PLACEMENT_ZONES ,
PLUGIN_LAUNCHER_ACTIONS ,
PLUGIN_LAUNCHER_BOUNDS ,
PLUGIN_LAUNCHER_RENDER_ENVIRONMENTS ,
PLUGIN_STATE_SCOPE_KINDS ,
} from "../constants.js" ;
// ---------------------------------------------------------------------------
// JSON Schema placeholder – a permissive validator for JSON Schema objects
// ---------------------------------------------------------------------------
/ * *
* Permissive validator for JSON Schema objects . Accepts any ` Record<string, unknown> `
* that contains at least a ` type ` , ` $ ref ` , or composition keyword ( ` oneOf ` / ` anyOf ` / ` allOf ` ) .
* Empty objects are also accepted .
*
* Used to validate ` instanceConfigSchema ` and ` parametersSchema ` fields in the
* plugin manifest without fully parsing JSON Schema .
*
* @see PLUGIN_SPEC . md § 10.1 — Manifest shape
* /
export const jsonSchemaSchema = z . record ( z . unknown ( ) ) . refine (
( val ) = > {
// Must have a "type" field if non-empty, or be a valid JSON Schema object
if ( Object . keys ( val ) . length === 0 ) return true ;
return typeof val . type === "string" || val . $ref !== undefined || val . oneOf !== undefined || val . anyOf !== undefined || val . allOf !== undefined ;
} ,
{ message : "Must be a valid JSON Schema object (requires at least a 'type', '$ref', or composition keyword)" } ,
) ;
// ---------------------------------------------------------------------------
// Manifest sub-type schemas
// ---------------------------------------------------------------------------
/ * *
* Validates a { @link PluginJobDeclaration } — a scheduled job declared in the
* plugin manifest . Requires ` jobKey ` and ` displayName ` ; ` description ` and
* ` schedule ` ( cron expression ) are optional .
*
* @see PLUGIN_SPEC . md § 17 — Scheduled Jobs
* /
/ * *
* Validates a cron expression has exactly 5 whitespace - separated fields ,
* each containing only valid cron characters ( digits , * , / , - , , ) .
*
* Valid tokens per field : * , N , N - M , N / S , * / S , N - M / S , a n d c o m m a - s e p a r a t e d l i s t s .
* /
const CRON_FIELD_PATTERN = /^(\*(?:\/[0-9]+)?|[0-9]+(?:-[0-9]+)?(?:\/[0-9]+)?)(?:,(\*(?:\/[0-9]+)?|[0-9]+(?:-[0-9]+)?(?:\/[0-9]+)?))*$/ ;
function isValidCronExpression ( expression : string ) : boolean {
const trimmed = expression . trim ( ) ;
if ( ! trimmed ) return false ;
const fields = trimmed . split ( /\s+/ ) ;
if ( fields . length !== 5 ) return false ;
return fields . every ( ( f ) = > CRON_FIELD_PATTERN . test ( f ) ) ;
}
export const pluginJobDeclarationSchema = z . object ( {
jobKey : z.string ( ) . min ( 1 ) ,
displayName : z.string ( ) . min ( 1 ) ,
description : z.string ( ) . optional ( ) ,
schedule : z.string ( ) . refine (
( val ) = > isValidCronExpression ( val ) ,
{ message : "schedule must be a valid 5-field cron expression (e.g. '*/15 * * * *')" } ,
) . optional ( ) ,
} ) ;
export type PluginJobDeclarationInput = z . infer < typeof pluginJobDeclarationSchema > ;
/ * *
* Validates a { @link PluginWebhookDeclaration } — a webhook endpoint declared
* in the plugin manifest . Requires ` endpointKey ` and ` displayName ` .
*
* @see PLUGIN_SPEC . md § 18 — Webhooks
* /
export const pluginWebhookDeclarationSchema = z . object ( {
endpointKey : z.string ( ) . min ( 1 ) ,
displayName : z.string ( ) . min ( 1 ) ,
description : z.string ( ) . optional ( ) ,
} ) ;
export type PluginWebhookDeclarationInput = z . infer < typeof pluginWebhookDeclarationSchema > ;
/ * *
* Validates a { @link PluginToolDeclaration } — an agent tool contributed by the
* plugin . Requires ` name ` , ` displayName ` , ` description ` , and a valid
* ` parametersSchema ` . Requires the ` agent.tools.register ` capability .
*
* @see PLUGIN_SPEC . md § 11 — Agent Tools
* /
export const pluginToolDeclarationSchema = z . object ( {
name : z.string ( ) . min ( 1 ) ,
displayName : z.string ( ) . min ( 1 ) ,
description : z.string ( ) . min ( 1 ) ,
parametersSchema : jsonSchemaSchema ,
} ) ;
export type PluginToolDeclarationInput = z . infer < typeof pluginToolDeclarationSchema > ;
/ * *
* Validates a { @link PluginUiSlotDeclaration } — a UI extension slot the plugin
* fills with a React component . Includes ` superRefine ` checks for slot - specific
* requirements such as ` entityTypes ` for context - sensitive slots .
*
* @see PLUGIN_SPEC . md § 19 — UI Extension Model
* /
export const pluginUiSlotDeclarationSchema = z . object ( {
type : z . enum ( PLUGIN_UI_SLOT_TYPES ) ,
id : z.string ( ) . min ( 1 ) ,
displayName : z.string ( ) . min ( 1 ) ,
exportName : z.string ( ) . min ( 1 ) ,
entityTypes : z.array ( z . enum ( PLUGIN_UI_SLOT_ENTITY_TYPES ) ) . optional ( ) ,
routePath : z.string ( ) . regex ( /^[a-z0-9][a-z0-9-]*$/ , {
message : "routePath must be a lowercase single-segment slug (letters, numbers, hyphens)" ,
} ) . optional ( ) ,
order : z.number ( ) . int ( ) . optional ( ) ,
} ) . superRefine ( ( value , ctx ) = > {
// context-sensitive slots require explicit entity targeting.
const entityScopedTypes = [ "detailTab" , "taskDetailView" , "contextMenuItem" , "commentAnnotation" , "commentContextMenuItem" , "projectSidebarItem" ] ;
if (
entityScopedTypes . includes ( value . type )
&& ( ! value . entityTypes || value . entityTypes . length === 0 )
) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` ${ value . type } slots require at least one entityType ` ,
path : [ "entityTypes" ] ,
} ) ;
}
// projectSidebarItem only makes sense for entityType "project".
if ( value . type === "projectSidebarItem" && value . entityTypes && ! value . entityTypes . includes ( "project" ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "projectSidebarItem slots require entityTypes to include \"project\"" ,
path : [ "entityTypes" ] ,
} ) ;
}
// commentAnnotation only makes sense for entityType "comment".
if ( value . type === "commentAnnotation" && value . entityTypes && ! value . entityTypes . includes ( "comment" ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "commentAnnotation slots require entityTypes to include \"comment\"" ,
path : [ "entityTypes" ] ,
} ) ;
}
// commentContextMenuItem only makes sense for entityType "comment".
if ( value . type === "commentContextMenuItem" && value . entityTypes && ! value . entityTypes . includes ( "comment" ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "commentContextMenuItem slots require entityTypes to include \"comment\"" ,
path : [ "entityTypes" ] ,
} ) ;
}
if ( value . routePath && value . type !== "page" ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "routePath is only supported for page slots" ,
path : [ "routePath" ] ,
} ) ;
}
if ( value . routePath && PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS . includes ( value . routePath as ( typeof PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS ) [ number ] ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` routePath " ${ value . routePath } " is reserved by the host ` ,
path : [ "routePath" ] ,
} ) ;
}
} ) ;
export type PluginUiSlotDeclarationInput = z . infer < typeof pluginUiSlotDeclarationSchema > ;
const entityScopedLauncherPlacementZones = [
"detailTab" ,
"taskDetailView" ,
"contextMenuItem" ,
"commentAnnotation" ,
"commentContextMenuItem" ,
"projectSidebarItem" ,
] as const ;
const launcherBoundsByEnvironment : Record <
( typeof PLUGIN_LAUNCHER_RENDER_ENVIRONMENTS ) [ number ] ,
readonly ( typeof PLUGIN_LAUNCHER_BOUNDS ) [ number ] [ ]
> = {
hostInline : [ "inline" , "compact" , "default" ] ,
hostOverlay : [ "compact" , "default" , "wide" , "full" ] ,
hostRoute : [ "default" , "wide" , "full" ] ,
external : [ ] ,
iframe : [ "compact" , "default" , "wide" , "full" ] ,
} ;
/ * *
* Validates the action payload for a declarative plugin launcher .
* /
export const pluginLauncherActionDeclarationSchema = z . object ( {
type : z . enum ( PLUGIN_LAUNCHER_ACTIONS ) ,
target : z.string ( ) . min ( 1 ) ,
params : z.record ( z . unknown ( ) ) . optional ( ) ,
} ) . superRefine ( ( value , ctx ) = > {
if ( value . type === "performAction" && value . target . includes ( "/" ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "performAction launchers must target an action key, not a route or URL" ,
path : [ "target" ] ,
} ) ;
}
if ( value . type === "navigate" && /^https?:\/\// . test ( value . target ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "navigate launchers must target a host route, not an absolute URL" ,
path : [ "target" ] ,
} ) ;
}
} ) ;
export type PluginLauncherActionDeclarationInput =
z . infer < typeof pluginLauncherActionDeclarationSchema > ;
/ * *
* Validates optional render hints for a plugin launcher destination .
* /
export const pluginLauncherRenderDeclarationSchema = z . object ( {
environment : z.enum ( PLUGIN_LAUNCHER_RENDER_ENVIRONMENTS ) ,
bounds : z.enum ( PLUGIN_LAUNCHER_BOUNDS ) . optional ( ) ,
} ) . superRefine ( ( value , ctx ) = > {
if ( ! value . bounds ) {
return ;
}
const supportedBounds = launcherBoundsByEnvironment [ value . environment ] ;
if ( ! supportedBounds . includes ( value . bounds ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` bounds " ${ value . bounds } " is not supported for render environment " ${ value . environment } " ` ,
path : [ "bounds" ] ,
} ) ;
}
} ) ;
export type PluginLauncherRenderDeclarationInput =
z . infer < typeof pluginLauncherRenderDeclarationSchema > ;
/ * *
* Validates declarative launcher metadata in a plugin manifest .
* /
export const pluginLauncherDeclarationSchema = z . object ( {
id : z.string ( ) . min ( 1 ) ,
displayName : z.string ( ) . min ( 1 ) ,
description : z.string ( ) . optional ( ) ,
placementZone : z.enum ( PLUGIN_LAUNCHER_PLACEMENT_ZONES ) ,
exportName : z.string ( ) . min ( 1 ) . optional ( ) ,
entityTypes : z.array ( z . enum ( PLUGIN_UI_SLOT_ENTITY_TYPES ) ) . optional ( ) ,
order : z.number ( ) . int ( ) . optional ( ) ,
action : pluginLauncherActionDeclarationSchema ,
render : pluginLauncherRenderDeclarationSchema.optional ( ) ,
} ) . superRefine ( ( value , ctx ) = > {
if (
entityScopedLauncherPlacementZones . some ( ( zone ) = > zone === value . placementZone )
&& ( ! value . entityTypes || value . entityTypes . length === 0 )
) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` ${ value . placementZone } launchers require at least one entityType ` ,
path : [ "entityTypes" ] ,
} ) ;
}
if (
value . placementZone === "projectSidebarItem"
&& value . entityTypes
&& ! value . entityTypes . includes ( "project" )
) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "projectSidebarItem launchers require entityTypes to include \"project\"" ,
path : [ "entityTypes" ] ,
} ) ;
}
if ( value . action . type === "performAction" && value . render ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "performAction launchers cannot declare render hints" ,
path : [ "render" ] ,
} ) ;
}
if (
[ "openModal" , "openDrawer" , "openPopover" ] . includes ( value . action . type )
&& ! value . render
) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` ${ value . action . type } launchers require render metadata ` ,
path : [ "render" ] ,
} ) ;
}
if ( value . action . type === "openModal" && value . render ? . environment === "hostInline" ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "openModal launchers cannot use the hostInline render environment" ,
path : [ "render" , "environment" ] ,
} ) ;
}
if (
value . action . type === "openDrawer"
&& value . render
&& ! [ "hostOverlay" , "iframe" ] . includes ( value . render . environment )
) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "openDrawer launchers must use hostOverlay or iframe render environments" ,
path : [ "render" , "environment" ] ,
} ) ;
}
if ( value . action . type === "openPopover" && value . render ? . environment === "hostRoute" ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "openPopover launchers cannot use the hostRoute render environment" ,
path : [ "render" , "environment" ] ,
} ) ;
}
} ) ;
export type PluginLauncherDeclarationInput = z . infer < typeof pluginLauncherDeclarationSchema > ;
// ---------------------------------------------------------------------------
// Plugin Manifest V1 schema
// ---------------------------------------------------------------------------
/ * *
* Zod schema for { @link PaperclipPluginManifestV1 } — the complete runtime
* validator for plugin manifests read at install time .
*
* Field - level constraints ( see PLUGIN_SPEC . md § 10.1 for the normative rules ) :
*
* | Field | Type | Constraints |
* | -- -- -- -- -- -- -- -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- |
* | ` id ` | string | ` ^[a-z0-9][a-z0-9._-]* $ ` |
* | ` apiVersion ` | literal 1 | must equal ` PLUGIN_API_VERSION ` |
* | ` version ` | string | semver ( ` \ d+ \ . \ d+ \ . \ d+ ` ) |
* | ` displayName ` | string | 1 – 100 chars |
* | ` description ` | string | 1 – 500 chars |
* | ` author ` | string | 1 – 200 chars |
* | ` categories ` | enum [ ] | at least one ; values from PLUGIN_CATEGORIES |
* | ` minimumHostVersion ` | string ? | semver lower bound if present , no leading ` v ` |
* | ` minimumPaperclipVersion ` | string ? | legacy alias of ` minimumHostVersion ` |
* | ` capabilities ` | enum [ ] | at least one ; values from PLUGIN_CAPABILITIES |
* | ` entrypoints.worker ` | string | min 1 char |
* | ` entrypoints.ui ` | string ? | required when ` ui.slots ` is declared |
*
* Cross - field rules enforced via ` superRefine ` :
* - ` entrypoints.ui ` required when ` ui.slots ` declared
* - ` agent.tools.register ` capability required when ` tools ` declared
* - ` jobs.schedule ` capability required when ` jobs ` declared
* - ` webhooks.receive ` capability required when ` webhooks ` declared
* - duplicate ` jobs[].jobKey ` values are rejected
* - duplicate ` webhooks[].endpointKey ` values are rejected
* - duplicate ` tools[].name ` values are rejected
* - duplicate ` ui.slots[].id ` values are rejected
*
* @see PLUGIN_SPEC . md § 10.1 — Manifest shape
* @see { @link PaperclipPluginManifestV1 } — the inferred TypeScript type
* /
export const pluginManifestV1Schema = z . object ( {
id : z.string ( ) . min ( 1 ) . regex (
/^[a-z0-9][a-z0-9._-]*$/ ,
"Plugin id must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, hyphens, or underscores" ,
) ,
apiVersion : z.literal ( 1 ) ,
version : z.string ( ) . min ( 1 ) . regex (
/^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/ ,
"Version must follow semver (e.g. 1.0.0 or 1.0.0-beta.1)" ,
) ,
displayName : z.string ( ) . min ( 1 ) . max ( 100 ) ,
description : z.string ( ) . min ( 1 ) . max ( 500 ) ,
author : z.string ( ) . min ( 1 ) . max ( 200 ) ,
categories : z.array ( z . enum ( PLUGIN_CATEGORIES ) ) . min ( 1 ) ,
minimumHostVersion : z.string ( ) . regex (
/^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/ ,
"minimumHostVersion must follow semver (e.g. 1.0.0)" ,
) . optional ( ) ,
minimumPaperclipVersion : z.string ( ) . regex (
/^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/ ,
"minimumPaperclipVersion must follow semver (e.g. 1.0.0)" ,
) . optional ( ) ,
capabilities : z.array ( z . enum ( PLUGIN_CAPABILITIES ) ) . min ( 1 ) ,
entrypoints : z.object ( {
worker : z.string ( ) . min ( 1 ) ,
ui : z.string ( ) . min ( 1 ) . optional ( ) ,
} ) ,
instanceConfigSchema : jsonSchemaSchema.optional ( ) ,
jobs : z.array ( pluginJobDeclarationSchema ) . optional ( ) ,
webhooks : z.array ( pluginWebhookDeclarationSchema ) . optional ( ) ,
tools : z.array ( pluginToolDeclarationSchema ) . optional ( ) ,
launchers : z.array ( pluginLauncherDeclarationSchema ) . optional ( ) ,
ui : z.object ( {
slots : z.array ( pluginUiSlotDeclarationSchema ) . min ( 1 ) . optional ( ) ,
launchers : z.array ( pluginLauncherDeclarationSchema ) . optional ( ) ,
} ) . optional ( ) ,
} ) . superRefine ( ( manifest , ctx ) = > {
// ── Entrypoint ↔ UI slot consistency ──────────────────────────────────
// Plugins that declare UI slots must also declare a UI entrypoint so the
// host knows where to load the bundle from (PLUGIN_SPEC.md §10.1).
const hasUiSlots = ( manifest . ui ? . slots ? . length ? ? 0 ) > 0 ;
const hasUiLaunchers = ( manifest . ui ? . launchers ? . length ? ? 0 ) > 0 ;
if ( ( hasUiSlots || hasUiLaunchers ) && ! manifest . entrypoints . ui ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "entrypoints.ui is required when ui.slots or ui.launchers are declared" ,
path : [ "entrypoints" , "ui" ] ,
} ) ;
}
if (
manifest . minimumHostVersion
&& manifest . minimumPaperclipVersion
&& manifest . minimumHostVersion !== manifest . minimumPaperclipVersion
) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "minimumHostVersion and minimumPaperclipVersion must match when both are declared" ,
path : [ "minimumHostVersion" ] ,
} ) ;
}
// ── Capability ↔ feature declaration consistency ───────────────────────
// The host enforces capabilities at install and runtime. A plugin must
// declare every capability it needs up-front; silently having more features
// than capabilities would cause runtime rejections.
// tools require agent.tools.register (PLUGIN_SPEC.md §11)
if ( manifest . tools && manifest . tools . length > 0 ) {
if ( ! manifest . capabilities . includes ( "agent.tools.register" ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "Capability 'agent.tools.register' is required when tools are declared" ,
path : [ "capabilities" ] ,
} ) ;
}
}
// jobs require jobs.schedule (PLUGIN_SPEC.md §17)
if ( manifest . jobs && manifest . jobs . length > 0 ) {
if ( ! manifest . capabilities . includes ( "jobs.schedule" ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "Capability 'jobs.schedule' is required when jobs are declared" ,
path : [ "capabilities" ] ,
} ) ;
}
}
// webhooks require webhooks.receive (PLUGIN_SPEC.md §18)
if ( manifest . webhooks && manifest . webhooks . length > 0 ) {
if ( ! manifest . capabilities . includes ( "webhooks.receive" ) ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : "Capability 'webhooks.receive' is required when webhooks are declared" ,
path : [ "capabilities" ] ,
} ) ;
}
}
// ── Uniqueness checks ──────────────────────────────────────────────────
// Duplicate keys within a plugin's own manifest are always a bug. The host
// would not know which declaration takes precedence, so we reject early.
// job keys must be unique within the plugin (used as identifiers in the DB)
if ( manifest . jobs ) {
const jobKeys = manifest . jobs . map ( ( j ) = > j . jobKey ) ;
const duplicates = jobKeys . filter ( ( key , i ) = > jobKeys . indexOf ( key ) !== i ) ;
if ( duplicates . length > 0 ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` Duplicate job keys: ${ [ . . . new Set ( duplicates ) ] . join ( ", " ) } ` ,
path : [ "jobs" ] ,
} ) ;
}
}
// webhook endpoint keys must be unique within the plugin (used in routes)
if ( manifest . webhooks ) {
const endpointKeys = manifest . webhooks . map ( ( w ) = > w . endpointKey ) ;
const duplicates = endpointKeys . filter ( ( key , i ) = > endpointKeys . indexOf ( key ) !== i ) ;
if ( duplicates . length > 0 ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` Duplicate webhook endpoint keys: ${ [ . . . new Set ( duplicates ) ] . join ( ", " ) } ` ,
path : [ "webhooks" ] ,
} ) ;
}
}
// tool names must be unique within the plugin (namespaced at runtime)
if ( manifest . tools ) {
const toolNames = manifest . tools . map ( ( t ) = > t . name ) ;
const duplicates = toolNames . filter ( ( name , i ) = > toolNames . indexOf ( name ) !== i ) ;
if ( duplicates . length > 0 ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` Duplicate tool names: ${ [ . . . new Set ( duplicates ) ] . join ( ", " ) } ` ,
path : [ "tools" ] ,
} ) ;
}
}
// UI slot ids must be unique within the plugin (namespaced at runtime)
if ( manifest . ui ) {
if ( manifest . ui . slots ) {
const slotIds = manifest . ui . slots . map ( ( s ) = > s . id ) ;
const duplicates = slotIds . filter ( ( id , i ) = > slotIds . indexOf ( id ) !== i ) ;
if ( duplicates . length > 0 ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` Duplicate UI slot ids: ${ [ . . . new Set ( duplicates ) ] . join ( ", " ) } ` ,
path : [ "ui" , "slots" ] ,
} ) ;
}
}
}
// launcher ids must be unique within the plugin
const allLaunchers = [
. . . ( manifest . launchers ? ? [ ] ) ,
. . . ( manifest . ui ? . launchers ? ? [ ] ) ,
] ;
if ( allLaunchers . length > 0 ) {
const launcherIds = allLaunchers . map ( ( launcher ) = > launcher . id ) ;
const duplicates = launcherIds . filter ( ( id , i ) = > launcherIds . indexOf ( id ) !== i ) ;
if ( duplicates . length > 0 ) {
ctx . addIssue ( {
code : z.ZodIssueCode.custom ,
message : ` Duplicate launcher ids: ${ [ . . . new Set ( duplicates ) ] . join ( ", " ) } ` ,
path : manifest.ui?.launchers ? [ "ui" , "launchers" ] : [ "launchers" ] ,
} ) ;
}
}
} ) ;
export type PluginManifestV1Input = z . infer < typeof pluginManifestV1Schema > ;
// ---------------------------------------------------------------------------
// Plugin installation / registration request
// ---------------------------------------------------------------------------
/ * *
* Schema for installing ( registering ) a plugin .
* The server receives the packageName and resolves the manifest from the
* installed package .
* /
export const installPluginSchema = z . object ( {
packageName : z.string ( ) . min ( 1 ) ,
version : z.string ( ) . min ( 1 ) . optional ( ) ,
/** Set by loader for local-path installs so the worker can be resolved. */
packagePath : z.string ( ) . min ( 1 ) . optional ( ) ,
} ) ;
export type InstallPlugin = z . infer < typeof installPluginSchema > ;
// ---------------------------------------------------------------------------
// Plugin config (instance configuration) schemas
// ---------------------------------------------------------------------------
/ * *
* Schema for creating or updating a plugin ' s instance configuration .
* configJson is validated permissively here ; runtime validation against
* the plugin ' s instanceConfigSchema is done at the service layer .
* /
export const upsertPluginConfigSchema = z . object ( {
configJson : z.record ( z . unknown ( ) ) ,
} ) ;
export type UpsertPluginConfig = z . infer < typeof upsertPluginConfigSchema > ;
/ * *
* Schema for partially updating a plugin ' s instance configuration .
* Allows a partial merge of config values .
* /
export const patchPluginConfigSchema = z . object ( {
configJson : z.record ( z . unknown ( ) ) ,
} ) ;
export type PatchPluginConfig = z . infer < typeof patchPluginConfigSchema > ;
// ---------------------------------------------------------------------------
// Plugin status update
// ---------------------------------------------------------------------------
/ * *
* Schema for updating a plugin ' s lifecycle status . Used by the lifecycle
* manager to persist state transitions .
*
* @see { @link PLUGIN_STATUSES } for the valid status values
* /
export const updatePluginStatusSchema = z . object ( {
status : z.enum ( PLUGIN_STATUSES ) ,
lastError : z.string ( ) . nullable ( ) . optional ( ) ,
} ) ;
export type UpdatePluginStatus = z . infer < typeof updatePluginStatusSchema > ;
// ---------------------------------------------------------------------------
// Plugin uninstall
// ---------------------------------------------------------------------------
/** Schema for the uninstall request. `removeData` controls hard vs soft delete. */
export const uninstallPluginSchema = z . object ( {
removeData : z.boolean ( ) . optional ( ) . default ( false ) ,
} ) ;
export type UninstallPlugin = z . infer < typeof uninstallPluginSchema > ;
// ---------------------------------------------------------------------------
// Plugin state (key-value storage) schemas
// ---------------------------------------------------------------------------
/ * *
* Schema for a plugin state scope key — identifies the exact location where
* state is stored . Used by the ` ctx.state.get() ` , ` ctx.state.set() ` , and
* ` ctx.state.delete() ` SDK methods .
*
* @see PLUGIN_SPEC . md § 21.3 ` plugin_state `
* /
export const pluginStateScopeKeySchema = z . object ( {
scopeKind : z.enum ( PLUGIN_STATE_SCOPE_KINDS ) ,
scopeId : z.string ( ) . min ( 1 ) . optional ( ) ,
namespace : z . string ( ) . min ( 1 ) . optional ( ) ,
stateKey : z.string ( ) . min ( 1 ) ,
} ) ;
export type PluginStateScopeKey = z . infer < typeof pluginStateScopeKeySchema > ;
/ * *
* Schema for setting a plugin state value .
* /
export const setPluginStateSchema = z . object ( {
scopeKind : z.enum ( PLUGIN_STATE_SCOPE_KINDS ) ,
scopeId : z.string ( ) . min ( 1 ) . optional ( ) ,
namespace : z . string ( ) . min ( 1 ) . optional ( ) ,
stateKey : z.string ( ) . min ( 1 ) ,
/** JSON-serializable value to store. */
value : z.unknown ( ) ,
} ) ;
export type SetPluginState = z . infer < typeof setPluginStateSchema > ;
/ * *
* Schema for querying plugin state entries . All fields are optional to allow
* flexible list queries ( e . g . all state for a plugin within a scope ) .
* /
export const listPluginStateSchema = z . object ( {
scopeKind : z.enum ( PLUGIN_STATE_SCOPE_KINDS ) . optional ( ) ,
scopeId : z.string ( ) . min ( 1 ) . optional ( ) ,
namespace : z . string ( ) . min ( 1 ) . optional ( ) ,
} ) ;
export type ListPluginState = z . infer < typeof listPluginStateSchema > ;