Add plugin framework and settings UI
This commit is contained in:
@@ -26,7 +26,6 @@ export const AGENT_ADAPTER_TYPES = [
|
||||
"http",
|
||||
"claude_local",
|
||||
"codex_local",
|
||||
"gemini_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
"cursor",
|
||||
@@ -213,6 +212,9 @@ export const LIVE_EVENT_TYPES = [
|
||||
"heartbeat.run.log",
|
||||
"agent.status",
|
||||
"activity.logged",
|
||||
"plugin.ui.updated",
|
||||
"plugin.worker.crashed",
|
||||
"plugin.worker.restarted",
|
||||
] as const;
|
||||
export type LiveEventType = (typeof LIVE_EVENT_TYPES)[number];
|
||||
|
||||
@@ -246,3 +248,311 @@ export const PERMISSION_KEYS = [
|
||||
"joins:approve",
|
||||
] as const;
|
||||
export type PermissionKey = (typeof PERMISSION_KEYS)[number];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin System — see doc/plugins/PLUGIN_SPEC.md for the full specification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The current version of the Plugin API contract.
|
||||
*
|
||||
* Increment this value whenever a breaking change is made to the plugin API
|
||||
* so that the host can reject incompatible plugin manifests.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §4 — Versioning
|
||||
*/
|
||||
export const PLUGIN_API_VERSION = 1 as const;
|
||||
|
||||
/**
|
||||
* Lifecycle statuses for an installed plugin.
|
||||
*
|
||||
* State machine: installed → ready | error, ready → disabled | error | upgrade_pending | uninstalled,
|
||||
* disabled → ready | uninstalled, error → ready | uninstalled,
|
||||
* upgrade_pending → ready | error | uninstalled, uninstalled → installed (reinstall).
|
||||
*
|
||||
* @see {@link PluginStatus} — inferred union type
|
||||
* @see PLUGIN_SPEC.md §21.3 `plugins.status`
|
||||
*/
|
||||
export const PLUGIN_STATUSES = [
|
||||
"installed",
|
||||
"ready",
|
||||
"disabled",
|
||||
"error",
|
||||
"upgrade_pending",
|
||||
"uninstalled",
|
||||
] as const;
|
||||
export type PluginStatus = (typeof PLUGIN_STATUSES)[number];
|
||||
|
||||
/**
|
||||
* Plugin classification categories. A plugin declares one or more categories
|
||||
* in its manifest to describe its primary purpose.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §6.2
|
||||
*/
|
||||
export const PLUGIN_CATEGORIES = [
|
||||
"connector",
|
||||
"workspace",
|
||||
"automation",
|
||||
"ui",
|
||||
] as const;
|
||||
export type PluginCategory = (typeof PLUGIN_CATEGORIES)[number];
|
||||
|
||||
/**
|
||||
* Named permissions the host grants to a plugin. Plugins declare required
|
||||
* capabilities in their manifest; the host enforces them at runtime via the
|
||||
* plugin capability validator.
|
||||
*
|
||||
* Grouped into: Data Read, Data Write, Plugin State, Runtime/Integration,
|
||||
* Agent Tools, and UI.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §15 — Capability Model
|
||||
*/
|
||||
export const PLUGIN_CAPABILITIES = [
|
||||
// Data Read
|
||||
"companies.read",
|
||||
"projects.read",
|
||||
"project.workspaces.read",
|
||||
"issues.read",
|
||||
"issue.comments.read",
|
||||
"agents.read",
|
||||
"goals.read",
|
||||
"goals.create",
|
||||
"goals.update",
|
||||
"activity.read",
|
||||
"costs.read",
|
||||
// Data Write
|
||||
"issues.create",
|
||||
"issues.update",
|
||||
"issue.comments.create",
|
||||
"agents.pause",
|
||||
"agents.resume",
|
||||
"agents.invoke",
|
||||
"agent.sessions.create",
|
||||
"agent.sessions.list",
|
||||
"agent.sessions.send",
|
||||
"agent.sessions.close",
|
||||
"assets.write",
|
||||
"assets.read",
|
||||
"activity.log.write",
|
||||
"metrics.write",
|
||||
// Plugin State
|
||||
"plugin.state.read",
|
||||
"plugin.state.write",
|
||||
// Runtime / Integration
|
||||
"events.subscribe",
|
||||
"events.emit",
|
||||
"jobs.schedule",
|
||||
"webhooks.receive",
|
||||
"http.outbound",
|
||||
"secrets.read-ref",
|
||||
// Agent Tools
|
||||
"agent.tools.register",
|
||||
// UI
|
||||
"instance.settings.register",
|
||||
"ui.sidebar.register",
|
||||
"ui.page.register",
|
||||
"ui.detailTab.register",
|
||||
"ui.dashboardWidget.register",
|
||||
"ui.commentAnnotation.register",
|
||||
"ui.action.register",
|
||||
] as const;
|
||||
export type PluginCapability = (typeof PLUGIN_CAPABILITIES)[number];
|
||||
|
||||
/**
|
||||
* UI extension slot types. Each slot type corresponds to a mount point in the
|
||||
* Paperclip UI where plugin components can be rendered.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19 — UI Extension Model
|
||||
*/
|
||||
export const PLUGIN_UI_SLOT_TYPES = [
|
||||
"page",
|
||||
"detailTab",
|
||||
"taskDetailView",
|
||||
"dashboardWidget",
|
||||
"sidebar",
|
||||
"sidebarPanel",
|
||||
"projectSidebarItem",
|
||||
"toolbarButton",
|
||||
"contextMenuItem",
|
||||
"commentAnnotation",
|
||||
"commentContextMenuItem",
|
||||
"settingsPage",
|
||||
] as const;
|
||||
export type PluginUiSlotType = (typeof PLUGIN_UI_SLOT_TYPES)[number];
|
||||
|
||||
/**
|
||||
* Launcher placement zones describe where a plugin-owned launcher can appear
|
||||
* in the host UI. These are intentionally aligned with current slot surfaces
|
||||
* so manifest authors can describe launch intent without coupling to a single
|
||||
* component implementation detail.
|
||||
*/
|
||||
export const PLUGIN_LAUNCHER_PLACEMENT_ZONES = [
|
||||
"page",
|
||||
"detailTab",
|
||||
"taskDetailView",
|
||||
"dashboardWidget",
|
||||
"sidebar",
|
||||
"sidebarPanel",
|
||||
"projectSidebarItem",
|
||||
"toolbarButton",
|
||||
"contextMenuItem",
|
||||
"commentAnnotation",
|
||||
"commentContextMenuItem",
|
||||
"settingsPage",
|
||||
] as const;
|
||||
export type PluginLauncherPlacementZone = (typeof PLUGIN_LAUNCHER_PLACEMENT_ZONES)[number];
|
||||
|
||||
/**
|
||||
* Launcher action kinds describe what the launcher does when activated.
|
||||
*/
|
||||
export const PLUGIN_LAUNCHER_ACTIONS = [
|
||||
"navigate",
|
||||
"openModal",
|
||||
"openDrawer",
|
||||
"openPopover",
|
||||
"performAction",
|
||||
"deepLink",
|
||||
] as const;
|
||||
export type PluginLauncherAction = (typeof PLUGIN_LAUNCHER_ACTIONS)[number];
|
||||
|
||||
/**
|
||||
* Optional size hints the host can use when rendering plugin-owned launcher
|
||||
* destinations such as overlays, drawers, or full page handoffs.
|
||||
*/
|
||||
export const PLUGIN_LAUNCHER_BOUNDS = [
|
||||
"inline",
|
||||
"compact",
|
||||
"default",
|
||||
"wide",
|
||||
"full",
|
||||
] as const;
|
||||
export type PluginLauncherBounds = (typeof PLUGIN_LAUNCHER_BOUNDS)[number];
|
||||
|
||||
/**
|
||||
* Render environments describe the container a launcher expects after it is
|
||||
* activated. The current host may map these to concrete UI primitives.
|
||||
*/
|
||||
export const PLUGIN_LAUNCHER_RENDER_ENVIRONMENTS = [
|
||||
"hostInline",
|
||||
"hostOverlay",
|
||||
"hostRoute",
|
||||
"external",
|
||||
"iframe",
|
||||
] as const;
|
||||
export type PluginLauncherRenderEnvironment =
|
||||
(typeof PLUGIN_LAUNCHER_RENDER_ENVIRONMENTS)[number];
|
||||
|
||||
/**
|
||||
* Entity types that a `detailTab` UI slot can attach to.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.3 — Detail Tabs
|
||||
*/
|
||||
export const PLUGIN_UI_SLOT_ENTITY_TYPES = [
|
||||
"project",
|
||||
"issue",
|
||||
"agent",
|
||||
"goal",
|
||||
"run",
|
||||
"comment",
|
||||
] as const;
|
||||
export type PluginUiSlotEntityType = (typeof PLUGIN_UI_SLOT_ENTITY_TYPES)[number];
|
||||
|
||||
/**
|
||||
* Scope kinds for plugin state storage. Determines the granularity at which
|
||||
* a plugin stores key-value state data.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §21.3 `plugin_state.scope_kind`
|
||||
*/
|
||||
export const PLUGIN_STATE_SCOPE_KINDS = [
|
||||
"instance",
|
||||
"company",
|
||||
"project",
|
||||
"project_workspace",
|
||||
"agent",
|
||||
"issue",
|
||||
"goal",
|
||||
"run",
|
||||
] as const;
|
||||
export type PluginStateScopeKind = (typeof PLUGIN_STATE_SCOPE_KINDS)[number];
|
||||
|
||||
/** Statuses for a plugin's scheduled job definition. */
|
||||
export const PLUGIN_JOB_STATUSES = [
|
||||
"active",
|
||||
"paused",
|
||||
"failed",
|
||||
] as const;
|
||||
export type PluginJobStatus = (typeof PLUGIN_JOB_STATUSES)[number];
|
||||
|
||||
/** Statuses for individual job run executions. */
|
||||
export const PLUGIN_JOB_RUN_STATUSES = [
|
||||
"pending",
|
||||
"queued",
|
||||
"running",
|
||||
"succeeded",
|
||||
"failed",
|
||||
"cancelled",
|
||||
] as const;
|
||||
export type PluginJobRunStatus = (typeof PLUGIN_JOB_RUN_STATUSES)[number];
|
||||
|
||||
/** What triggered a particular job run. */
|
||||
export const PLUGIN_JOB_RUN_TRIGGERS = [
|
||||
"schedule",
|
||||
"manual",
|
||||
"retry",
|
||||
] as const;
|
||||
export type PluginJobRunTrigger = (typeof PLUGIN_JOB_RUN_TRIGGERS)[number];
|
||||
|
||||
/** Statuses for inbound webhook deliveries. */
|
||||
export const PLUGIN_WEBHOOK_DELIVERY_STATUSES = [
|
||||
"pending",
|
||||
"success",
|
||||
"failed",
|
||||
] as const;
|
||||
export type PluginWebhookDeliveryStatus = (typeof PLUGIN_WEBHOOK_DELIVERY_STATUSES)[number];
|
||||
|
||||
/**
|
||||
* Core domain event types that plugins can subscribe to via the
|
||||
* `events.subscribe` capability.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §16 — Event System
|
||||
*/
|
||||
export const PLUGIN_EVENT_TYPES = [
|
||||
"company.created",
|
||||
"company.updated",
|
||||
"project.created",
|
||||
"project.updated",
|
||||
"project.workspace_created",
|
||||
"project.workspace_updated",
|
||||
"project.workspace_deleted",
|
||||
"issue.created",
|
||||
"issue.updated",
|
||||
"issue.comment.created",
|
||||
"agent.created",
|
||||
"agent.updated",
|
||||
"agent.status_changed",
|
||||
"agent.run.started",
|
||||
"agent.run.finished",
|
||||
"agent.run.failed",
|
||||
"agent.run.cancelled",
|
||||
"goal.created",
|
||||
"goal.updated",
|
||||
"approval.created",
|
||||
"approval.decided",
|
||||
"cost_event.created",
|
||||
"activity.logged",
|
||||
] as const;
|
||||
export type PluginEventType = (typeof PLUGIN_EVENT_TYPES)[number];
|
||||
|
||||
/**
|
||||
* Error codes returned by the plugin bridge when a UI → worker call fails.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||||
*/
|
||||
export const PLUGIN_BRIDGE_ERROR_CODES = [
|
||||
"WORKER_UNAVAILABLE",
|
||||
"CAPABILITY_DENIED",
|
||||
"WORKER_ERROR",
|
||||
"TIMEOUT",
|
||||
"UNKNOWN",
|
||||
] as const;
|
||||
export type PluginBridgeErrorCode = (typeof PLUGIN_BRIDGE_ERROR_CODES)[number];
|
||||
|
||||
@@ -31,6 +31,23 @@ export {
|
||||
JOIN_REQUEST_TYPES,
|
||||
JOIN_REQUEST_STATUSES,
|
||||
PERMISSION_KEYS,
|
||||
PLUGIN_API_VERSION,
|
||||
PLUGIN_STATUSES,
|
||||
PLUGIN_CATEGORIES,
|
||||
PLUGIN_CAPABILITIES,
|
||||
PLUGIN_UI_SLOT_TYPES,
|
||||
PLUGIN_UI_SLOT_ENTITY_TYPES,
|
||||
PLUGIN_LAUNCHER_PLACEMENT_ZONES,
|
||||
PLUGIN_LAUNCHER_ACTIONS,
|
||||
PLUGIN_LAUNCHER_BOUNDS,
|
||||
PLUGIN_LAUNCHER_RENDER_ENVIRONMENTS,
|
||||
PLUGIN_STATE_SCOPE_KINDS,
|
||||
PLUGIN_JOB_STATUSES,
|
||||
PLUGIN_JOB_RUN_STATUSES,
|
||||
PLUGIN_JOB_RUN_TRIGGERS,
|
||||
PLUGIN_WEBHOOK_DELIVERY_STATUSES,
|
||||
PLUGIN_EVENT_TYPES,
|
||||
PLUGIN_BRIDGE_ERROR_CODES,
|
||||
type CompanyStatus,
|
||||
type DeploymentMode,
|
||||
type DeploymentExposure,
|
||||
@@ -61,6 +78,22 @@ export {
|
||||
type JoinRequestType,
|
||||
type JoinRequestStatus,
|
||||
type PermissionKey,
|
||||
type PluginStatus,
|
||||
type PluginCategory,
|
||||
type PluginCapability,
|
||||
type PluginUiSlotType,
|
||||
type PluginUiSlotEntityType,
|
||||
type PluginLauncherPlacementZone,
|
||||
type PluginLauncherAction,
|
||||
type PluginLauncherBounds,
|
||||
type PluginLauncherRenderEnvironment,
|
||||
type PluginStateScopeKind,
|
||||
type PluginJobStatus,
|
||||
type PluginJobRunStatus,
|
||||
type PluginJobRunTrigger,
|
||||
type PluginWebhookDeliveryStatus,
|
||||
type PluginEventType,
|
||||
type PluginBridgeErrorCode,
|
||||
} from "./constants.js";
|
||||
|
||||
export type {
|
||||
@@ -129,6 +162,28 @@ export type {
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
SecretProviderDescriptor,
|
||||
JsonSchema,
|
||||
PluginJobDeclaration,
|
||||
PluginWebhookDeclaration,
|
||||
PluginToolDeclaration,
|
||||
PluginUiSlotDeclaration,
|
||||
PluginLauncherActionDeclaration,
|
||||
PluginLauncherRenderDeclaration,
|
||||
PluginLauncherRenderContextSnapshot,
|
||||
PluginLauncherDeclaration,
|
||||
PluginMinimumHostVersion,
|
||||
PluginUiDeclaration,
|
||||
PaperclipPluginManifestV1,
|
||||
PluginRecord,
|
||||
PluginStateRecord,
|
||||
PluginConfig,
|
||||
PluginCompanySettings,
|
||||
CompanyPluginAvailability,
|
||||
PluginEntityRecord,
|
||||
PluginEntityQuery,
|
||||
PluginJobRecord,
|
||||
PluginJobRunRecord,
|
||||
PluginWebhookDeliveryRecord,
|
||||
} from "./types/index.js";
|
||||
|
||||
export {
|
||||
@@ -238,6 +293,45 @@ export {
|
||||
type CompanyPortabilityExport,
|
||||
type CompanyPortabilityPreview,
|
||||
type CompanyPortabilityImport,
|
||||
jsonSchemaSchema,
|
||||
pluginJobDeclarationSchema,
|
||||
pluginWebhookDeclarationSchema,
|
||||
pluginToolDeclarationSchema,
|
||||
pluginUiSlotDeclarationSchema,
|
||||
pluginLauncherActionDeclarationSchema,
|
||||
pluginLauncherRenderDeclarationSchema,
|
||||
pluginLauncherDeclarationSchema,
|
||||
pluginManifestV1Schema,
|
||||
installPluginSchema,
|
||||
upsertPluginConfigSchema,
|
||||
patchPluginConfigSchema,
|
||||
upsertPluginCompanySettingsSchema,
|
||||
updateCompanyPluginAvailabilitySchema,
|
||||
listCompanyPluginAvailabilitySchema,
|
||||
updatePluginStatusSchema,
|
||||
uninstallPluginSchema,
|
||||
pluginStateScopeKeySchema,
|
||||
setPluginStateSchema,
|
||||
listPluginStateSchema,
|
||||
type PluginJobDeclarationInput,
|
||||
type PluginWebhookDeclarationInput,
|
||||
type PluginToolDeclarationInput,
|
||||
type PluginUiSlotDeclarationInput,
|
||||
type PluginLauncherActionDeclarationInput,
|
||||
type PluginLauncherRenderDeclarationInput,
|
||||
type PluginLauncherDeclarationInput,
|
||||
type PluginManifestV1Input,
|
||||
type InstallPlugin,
|
||||
type UpsertPluginConfig,
|
||||
type PatchPluginConfig,
|
||||
type UpsertPluginCompanySettings,
|
||||
type UpdateCompanyPluginAvailability,
|
||||
type ListCompanyPluginAvailability,
|
||||
type UpdatePluginStatus,
|
||||
type UninstallPlugin,
|
||||
type PluginStateScopeKey,
|
||||
type SetPluginState,
|
||||
type ListPluginState,
|
||||
} from "./validators/index.js";
|
||||
|
||||
export { API_PREFIX, API } from "./api.js";
|
||||
|
||||
@@ -79,3 +79,27 @@ export type {
|
||||
CompanyPortabilityImportResult,
|
||||
CompanyPortabilityExportRequest,
|
||||
} from "./company-portability.js";
|
||||
export type {
|
||||
JsonSchema,
|
||||
PluginJobDeclaration,
|
||||
PluginWebhookDeclaration,
|
||||
PluginToolDeclaration,
|
||||
PluginUiSlotDeclaration,
|
||||
PluginLauncherActionDeclaration,
|
||||
PluginLauncherRenderDeclaration,
|
||||
PluginLauncherRenderContextSnapshot,
|
||||
PluginLauncherDeclaration,
|
||||
PluginMinimumHostVersion,
|
||||
PluginUiDeclaration,
|
||||
PaperclipPluginManifestV1,
|
||||
PluginRecord,
|
||||
PluginStateRecord,
|
||||
PluginConfig,
|
||||
PluginCompanySettings,
|
||||
CompanyPluginAvailability,
|
||||
PluginEntityRecord,
|
||||
PluginEntityQuery,
|
||||
PluginJobRecord,
|
||||
PluginJobRunRecord,
|
||||
PluginWebhookDeliveryRecord,
|
||||
} from "./plugin.js";
|
||||
|
||||
545
packages/shared/src/types/plugin.ts
Normal file
545
packages/shared/src/types/plugin.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
import type {
|
||||
PluginStatus,
|
||||
PluginCategory,
|
||||
PluginCapability,
|
||||
PluginUiSlotType,
|
||||
PluginUiSlotEntityType,
|
||||
PluginStateScopeKind,
|
||||
PluginLauncherPlacementZone,
|
||||
PluginLauncherAction,
|
||||
PluginLauncherBounds,
|
||||
PluginLauncherRenderEnvironment,
|
||||
} from "../constants.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON Schema placeholder – plugins declare config schemas as JSON Schema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A JSON Schema object used for plugin config schemas and tool parameter schemas.
|
||||
* Plugins provide these as plain JSON Schema compatible objects.
|
||||
*/
|
||||
export type JsonSchema = Record<string, unknown>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Manifest sub-types — nested declarations within PaperclipPluginManifestV1
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Declares a scheduled job a plugin can run.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §17 — Scheduled Jobs
|
||||
*/
|
||||
export interface PluginJobDeclaration {
|
||||
/** Stable identifier for this job, unique within the plugin. */
|
||||
jobKey: string;
|
||||
/** Human-readable name shown in the operator UI. */
|
||||
displayName: string;
|
||||
/** Optional description of what the job does. */
|
||||
description?: string;
|
||||
/** Cron expression for the schedule (e.g. "star/15 star star star star" or "0 * * * *"). */
|
||||
schedule?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a webhook endpoint the plugin can receive.
|
||||
* Route: `POST /api/plugins/:pluginId/webhooks/:endpointKey`
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §18 — Webhooks
|
||||
*/
|
||||
export interface PluginWebhookDeclaration {
|
||||
/** Stable identifier for this endpoint, unique within the plugin. */
|
||||
endpointKey: string;
|
||||
/** Human-readable name shown in the operator UI. */
|
||||
displayName: string;
|
||||
/** Optional description of what this webhook handles. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares an agent tool contributed by the plugin. Tools are namespaced
|
||||
* by plugin ID at runtime (e.g. `linear:search-issues`).
|
||||
*
|
||||
* Requires the `agent.tools.register` capability.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §11 — Agent Tools
|
||||
*/
|
||||
export interface PluginToolDeclaration {
|
||||
/** Tool name, unique within the plugin. Namespaced by plugin ID at runtime. */
|
||||
name: string;
|
||||
/** Human-readable name shown to agents and in the UI. */
|
||||
displayName: string;
|
||||
/** Description provided to the agent so it knows when to use this tool. */
|
||||
description: string;
|
||||
/** JSON Schema describing the tool's input parameters. */
|
||||
parametersSchema: JsonSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a UI extension slot the plugin fills with a React component.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19 — UI Extension Model
|
||||
*/
|
||||
export interface PluginUiSlotDeclaration {
|
||||
/** The type of UI mount point (page, detailTab, taskDetailView, toolbarButton, etc.). */
|
||||
type: PluginUiSlotType;
|
||||
/** Unique slot identifier within the plugin. */
|
||||
id: string;
|
||||
/** Human-readable name shown in navigation or tab labels. */
|
||||
displayName: string;
|
||||
/** Which export name in the UI bundle provides this component. */
|
||||
exportName: string;
|
||||
/**
|
||||
* Entity targets for context-sensitive slots.
|
||||
* Required for `detailTab`, `taskDetailView`, and `contextMenuItem`.
|
||||
*/
|
||||
entityTypes?: PluginUiSlotEntityType[];
|
||||
/**
|
||||
* Optional ordering hint within a slot surface. Lower numbers appear first.
|
||||
* Defaults to host-defined ordering if omitted.
|
||||
*/
|
||||
order?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the action triggered by a plugin launcher surface.
|
||||
*/
|
||||
export interface PluginLauncherActionDeclaration {
|
||||
/** What kind of launch behavior the host should perform. */
|
||||
type: PluginLauncherAction;
|
||||
/**
|
||||
* Stable target identifier or URL. The meaning depends on `type`
|
||||
* (for example a route, tab key, action key, or external URL).
|
||||
*/
|
||||
target: string;
|
||||
/** Optional arbitrary parameters passed along to the target. */
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional render metadata for the destination opened by a launcher.
|
||||
*/
|
||||
export interface PluginLauncherRenderDeclaration {
|
||||
/** High-level container the launcher expects the host to use. */
|
||||
environment: PluginLauncherRenderEnvironment;
|
||||
/** Optional size hint for the destination surface. */
|
||||
bounds?: PluginLauncherBounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable runtime snapshot of the host launcher/container environment.
|
||||
*/
|
||||
export interface PluginLauncherRenderContextSnapshot {
|
||||
/** The current launcher/container environment selected by the host. */
|
||||
environment: PluginLauncherRenderEnvironment | null;
|
||||
/** Launcher id that opened this surface, if any. */
|
||||
launcherId: string | null;
|
||||
/** Current host-applied bounds hint for the environment, if any. */
|
||||
bounds: PluginLauncherBounds | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a plugin launcher surface independent of the low-level slot
|
||||
* implementation that mounts it.
|
||||
*/
|
||||
export interface PluginLauncherDeclaration {
|
||||
/** Stable identifier for this launcher, unique within the plugin. */
|
||||
id: string;
|
||||
/** Human-readable label shown for the launcher. */
|
||||
displayName: string;
|
||||
/** Optional description for operator-facing docs or future UI affordances. */
|
||||
description?: string;
|
||||
/** Where in the host UI this launcher should be placed. */
|
||||
placementZone: PluginLauncherPlacementZone;
|
||||
/** Optional export name in the UI bundle when the launcher has custom UI. */
|
||||
exportName?: string;
|
||||
/**
|
||||
* Optional entity targeting for context-sensitive launcher zones.
|
||||
* Reuses the same entity union as UI slots for consistency.
|
||||
*/
|
||||
entityTypes?: PluginUiSlotEntityType[];
|
||||
/** Optional ordering hint within the placement zone. */
|
||||
order?: number;
|
||||
/** What should happen when the launcher is activated. */
|
||||
action: PluginLauncherActionDeclaration;
|
||||
/** Optional render/container hints for the launched destination. */
|
||||
render?: PluginLauncherRenderDeclaration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lower-bound semver requirement for the Paperclip host.
|
||||
*
|
||||
* The host should reject installation when its running version is lower than
|
||||
* the declared minimum.
|
||||
*/
|
||||
export type PluginMinimumHostVersion = string;
|
||||
|
||||
/**
|
||||
* Groups plugin UI declarations that are served from the shared UI bundle
|
||||
* root declared in `entrypoints.ui`.
|
||||
*/
|
||||
export interface PluginUiDeclaration {
|
||||
/** UI extension slots this plugin fills. */
|
||||
slots?: PluginUiSlotDeclaration[];
|
||||
/** Declarative launcher metadata for host-mounted plugin entry points. */
|
||||
launchers?: PluginLauncherDeclaration[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Manifest V1
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The manifest shape every plugin package must export.
|
||||
* See PLUGIN_SPEC.md §10.1 for the normative definition.
|
||||
*/
|
||||
export interface PaperclipPluginManifestV1 {
|
||||
/** Globally unique plugin identifier (e.g. `"acme.linear-sync"`). Must be lowercase alphanumeric with dots, hyphens, or underscores. */
|
||||
id: string;
|
||||
/** Plugin API version. Must be `1` for the current spec. */
|
||||
apiVersion: 1;
|
||||
/** Semver version of the plugin package (e.g. `"1.2.0"`). */
|
||||
version: string;
|
||||
/** Human-readable name (max 100 chars). */
|
||||
displayName: string;
|
||||
/** Short description (max 500 chars). */
|
||||
description: string;
|
||||
/** Author name (max 200 chars). May include email in angle brackets, e.g. `"Jane Doe <jane@example.com>"`. */
|
||||
author: string;
|
||||
/** One or more categories classifying this plugin. */
|
||||
categories: PluginCategory[];
|
||||
/**
|
||||
* Minimum host version required (semver lower bound).
|
||||
* Preferred generic field for new manifests.
|
||||
*/
|
||||
minimumHostVersion?: PluginMinimumHostVersion;
|
||||
/**
|
||||
* Legacy alias for `minimumHostVersion`.
|
||||
* Kept for backwards compatibility with existing manifests and docs.
|
||||
*/
|
||||
minimumPaperclipVersion?: PluginMinimumHostVersion;
|
||||
/** Capabilities this plugin requires from the host. Enforced at runtime. */
|
||||
capabilities: PluginCapability[];
|
||||
/** Entrypoint paths relative to the package root. */
|
||||
entrypoints: {
|
||||
/** Path to the worker entrypoint (required). */
|
||||
worker: string;
|
||||
/** Path to the UI bundle directory (required when `ui.slots` is declared). */
|
||||
ui?: string;
|
||||
};
|
||||
/** JSON Schema for operator-editable instance configuration. */
|
||||
instanceConfigSchema?: JsonSchema;
|
||||
/** Scheduled jobs this plugin declares. Requires `jobs.schedule` capability. */
|
||||
jobs?: PluginJobDeclaration[];
|
||||
/** Webhook endpoints this plugin declares. Requires `webhooks.receive` capability. */
|
||||
webhooks?: PluginWebhookDeclaration[];
|
||||
/** Agent tools this plugin contributes. Requires `agent.tools.register` capability. */
|
||||
tools?: PluginToolDeclaration[];
|
||||
/**
|
||||
* Legacy top-level launcher declarations.
|
||||
* Prefer `ui.launchers` for new manifests.
|
||||
*/
|
||||
launchers?: PluginLauncherDeclaration[];
|
||||
/** UI bundle declarations. Requires `entrypoints.ui` when populated. */
|
||||
ui?: PluginUiDeclaration;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Record – represents a row in the `plugins` table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for an installed plugin as persisted in the `plugins` table.
|
||||
* See PLUGIN_SPEC.md §21.3 for the schema definition.
|
||||
*/
|
||||
export interface PluginRecord {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** Unique key derived from `manifest.id`. Used for lookups. */
|
||||
pluginKey: string;
|
||||
/** npm package name (e.g. `"@acme/plugin-linear"`). */
|
||||
packageName: string;
|
||||
/** Installed semver version. */
|
||||
version: string;
|
||||
/** Plugin API version from the manifest. */
|
||||
apiVersion: number;
|
||||
/** Plugin categories from the manifest. */
|
||||
categories: PluginCategory[];
|
||||
/** Full manifest snapshot persisted at install/upgrade time. */
|
||||
manifestJson: PaperclipPluginManifestV1;
|
||||
/** Current lifecycle status. */
|
||||
status: PluginStatus;
|
||||
/** Deterministic load order (null if not yet assigned). */
|
||||
installOrder: number | null;
|
||||
/** Resolved package path for local-path installs; used to find worker entrypoint. */
|
||||
packagePath: string | null;
|
||||
/** Most recent error message, or operator-provided disable reason. */
|
||||
lastError: string | null;
|
||||
/** Timestamp when the plugin was first installed. */
|
||||
installedAt: Date;
|
||||
/** Timestamp of the most recent status or metadata change. */
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin State – represents a row in the `plugin_state` table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for a single scoped key-value entry in the `plugin_state` table.
|
||||
* Plugins read and write these entries through `ctx.state` in the SDK.
|
||||
*
|
||||
* The five-part composite key `(pluginId, scopeKind, scopeId, namespace, stateKey)`
|
||||
* uniquely identifies a state entry.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §21.3 — `plugin_state`
|
||||
*/
|
||||
export interface PluginStateRecord {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** FK to `plugins.id`. */
|
||||
pluginId: string;
|
||||
/** Granularity of the scope. */
|
||||
scopeKind: PluginStateScopeKind;
|
||||
/**
|
||||
* UUID or text identifier for the scoped object.
|
||||
* `null` for `instance` scope (no associated entity).
|
||||
*/
|
||||
scopeId: string | null;
|
||||
/**
|
||||
* Sub-namespace within the scope to avoid key collisions.
|
||||
* Defaults to `"default"` if not explicitly set by the plugin.
|
||||
*/
|
||||
namespace: string;
|
||||
/** The key for this state entry within the namespace. */
|
||||
stateKey: string;
|
||||
/** Stored JSON value. May be any JSON-serializable type. */
|
||||
valueJson: unknown;
|
||||
/** Timestamp of the most recent write. */
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Config – represents a row in the `plugin_config` table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for a plugin's instance configuration as persisted in the
|
||||
* `plugin_config` table.
|
||||
* See PLUGIN_SPEC.md §21.3 for the schema definition.
|
||||
*/
|
||||
export interface PluginConfig {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** FK to `plugins.id`. Unique — each plugin has at most one config row. */
|
||||
pluginId: string;
|
||||
/** Operator-provided configuration values (validated against `instanceConfigSchema`). */
|
||||
configJson: Record<string, unknown>;
|
||||
/** Most recent config validation error, if any. */
|
||||
lastError: string | null;
|
||||
/** Timestamp when the config row was created. */
|
||||
createdAt: Date;
|
||||
/** Timestamp of the most recent config update. */
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Company Plugin Availability / Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for a plugin's company-scoped settings row as persisted in the
|
||||
* `plugin_company_settings` table.
|
||||
*
|
||||
* This is separate from instance-wide `PluginConfig`: the plugin remains
|
||||
* installed globally, while each company can store its own plugin settings and
|
||||
* availability state independently.
|
||||
*/
|
||||
export interface PluginCompanySettings {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** FK to `companies.id`. */
|
||||
companyId: string;
|
||||
/** FK to `plugins.id`. */
|
||||
pluginId: string;
|
||||
/** Explicit availability override for this company/plugin pair. */
|
||||
enabled: boolean;
|
||||
/** Company-scoped plugin settings payload. */
|
||||
settingsJson: Record<string, unknown>;
|
||||
/** Most recent company-scoped validation or availability error, if any. */
|
||||
lastError: string | null;
|
||||
/** Timestamp when the settings row was created. */
|
||||
createdAt: Date;
|
||||
/** Timestamp of the most recent settings update. */
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* API response shape describing whether a plugin is available to a specific
|
||||
* company and, when present, the company-scoped settings row backing that
|
||||
* availability.
|
||||
*/
|
||||
export interface CompanyPluginAvailability {
|
||||
companyId: string;
|
||||
pluginId: string;
|
||||
/** Stable manifest/plugin key for display and route generation. */
|
||||
pluginKey: string;
|
||||
/** Human-readable plugin name. */
|
||||
pluginDisplayName: string;
|
||||
/** Current instance-wide plugin lifecycle status. */
|
||||
pluginStatus: PluginStatus;
|
||||
/**
|
||||
* Whether the plugin is currently available to the company.
|
||||
* When no `plugin_company_settings` row exists yet, the plugin is enabled
|
||||
* by default for the company.
|
||||
*/
|
||||
available: boolean;
|
||||
/** Company-scoped settings, defaulting to an empty object when unavailable. */
|
||||
settingsJson: Record<string, unknown>;
|
||||
/** Most recent company-scoped error, if any. */
|
||||
lastError: string | null;
|
||||
/** Present when availability is backed by a persisted settings row. */
|
||||
createdAt: Date | null;
|
||||
/** Present when availability is backed by a persisted settings row. */
|
||||
updatedAt: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query filter for `ctx.entities.list`.
|
||||
*/
|
||||
export interface PluginEntityQuery {
|
||||
/** Optional filter by entity type (e.g. 'project', 'issue'). */
|
||||
entityType?: string;
|
||||
/** Optional filter by external system identifier. */
|
||||
externalId?: string;
|
||||
/** Maximum number of records to return. Defaults to 100. */
|
||||
limit?: number;
|
||||
/** Number of records to skip. Defaults to 0. */
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Entity – represents a row in the `plugin_entities` table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for an external entity mapping as persisted in the `plugin_entities` table.
|
||||
*/
|
||||
export interface PluginEntityRecord {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** FK to `plugins.id`. */
|
||||
pluginId: string;
|
||||
/** Plugin-defined entity type. */
|
||||
entityType: string;
|
||||
/** Scope where this entity lives. */
|
||||
scopeKind: PluginStateScopeKind;
|
||||
/** UUID or text identifier for the scoped object. */
|
||||
scopeId: string | null;
|
||||
/** External identifier in the remote system. */
|
||||
externalId: string | null;
|
||||
/** Human-readable title. */
|
||||
title: string | null;
|
||||
/** Optional status string. */
|
||||
status: string | null;
|
||||
/** Full entity data blob. */
|
||||
data: Record<string, unknown>;
|
||||
/** ISO 8601 creation timestamp. */
|
||||
createdAt: Date;
|
||||
/** ISO 8601 last-updated timestamp. */
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Job – represents a row in the `plugin_jobs` table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for a registered plugin job as persisted in the `plugin_jobs` table.
|
||||
*/
|
||||
export interface PluginJobRecord {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** FK to `plugins.id`. */
|
||||
pluginId: string;
|
||||
/** Job key matching the manifest declaration. */
|
||||
jobKey: string;
|
||||
/** Cron expression for the schedule. */
|
||||
schedule: string;
|
||||
/** Current job status. */
|
||||
status: "active" | "paused" | "failed";
|
||||
/** Last time the job was executed. */
|
||||
lastRunAt: Date | null;
|
||||
/** Next scheduled execution time. */
|
||||
nextRunAt: Date | null;
|
||||
/** ISO 8601 creation timestamp. */
|
||||
createdAt: Date;
|
||||
/** ISO 8601 last-updated timestamp. */
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Job Run – represents a row in the `plugin_job_runs` table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for a job execution history record.
|
||||
*/
|
||||
export interface PluginJobRunRecord {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** FK to `plugin_jobs.id`. */
|
||||
jobId: string;
|
||||
/** FK to `plugins.id`. */
|
||||
pluginId: string;
|
||||
/** What triggered this run. */
|
||||
trigger: "schedule" | "manual" | "retry";
|
||||
/** Current run status. */
|
||||
status: "pending" | "queued" | "running" | "succeeded" | "failed" | "cancelled";
|
||||
/** Run duration in milliseconds. */
|
||||
durationMs: number | null;
|
||||
/** Error message if the run failed. */
|
||||
error: string | null;
|
||||
/** Run logs. */
|
||||
logs: string[];
|
||||
/** ISO 8601 start timestamp. */
|
||||
startedAt: Date | null;
|
||||
/** ISO 8601 finish timestamp. */
|
||||
finishedAt: Date | null;
|
||||
/** ISO 8601 creation timestamp. */
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Webhook Delivery – represents a row in the `plugin_webhook_deliveries` table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Domain type for an inbound webhook delivery record.
|
||||
*/
|
||||
export interface PluginWebhookDeliveryRecord {
|
||||
/** UUID primary key. */
|
||||
id: string;
|
||||
/** FK to `plugins.id`. */
|
||||
pluginId: string;
|
||||
/** Webhook endpoint key matching the manifest. */
|
||||
webhookKey: string;
|
||||
/** External identifier from the remote system. */
|
||||
externalId: string | null;
|
||||
/** Delivery status. */
|
||||
status: "pending" | "success" | "failed";
|
||||
/** Processing duration in milliseconds. */
|
||||
durationMs: number | null;
|
||||
/** Error message if processing failed. */
|
||||
error: string | null;
|
||||
/** Webhook payload. */
|
||||
payload: Record<string, unknown>;
|
||||
/** Webhook headers. */
|
||||
headers: Record<string, string>;
|
||||
/** ISO 8601 start timestamp. */
|
||||
startedAt: Date | null;
|
||||
/** ISO 8601 finish timestamp. */
|
||||
finishedAt: Date | null;
|
||||
/** ISO 8601 creation timestamp. */
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -137,3 +137,45 @@ export {
|
||||
type UpdateMemberPermissions,
|
||||
type UpdateUserCompanyAccess,
|
||||
} from "./access.js";
|
||||
|
||||
export {
|
||||
jsonSchemaSchema,
|
||||
pluginJobDeclarationSchema,
|
||||
pluginWebhookDeclarationSchema,
|
||||
pluginToolDeclarationSchema,
|
||||
pluginUiSlotDeclarationSchema,
|
||||
pluginLauncherActionDeclarationSchema,
|
||||
pluginLauncherRenderDeclarationSchema,
|
||||
pluginLauncherDeclarationSchema,
|
||||
pluginManifestV1Schema,
|
||||
installPluginSchema,
|
||||
upsertPluginConfigSchema,
|
||||
patchPluginConfigSchema,
|
||||
upsertPluginCompanySettingsSchema,
|
||||
updateCompanyPluginAvailabilitySchema,
|
||||
listCompanyPluginAvailabilitySchema,
|
||||
updatePluginStatusSchema,
|
||||
uninstallPluginSchema,
|
||||
pluginStateScopeKeySchema,
|
||||
setPluginStateSchema,
|
||||
listPluginStateSchema,
|
||||
type PluginJobDeclarationInput,
|
||||
type PluginWebhookDeclarationInput,
|
||||
type PluginToolDeclarationInput,
|
||||
type PluginUiSlotDeclarationInput,
|
||||
type PluginLauncherActionDeclarationInput,
|
||||
type PluginLauncherRenderDeclarationInput,
|
||||
type PluginLauncherDeclarationInput,
|
||||
type PluginManifestV1Input,
|
||||
type InstallPlugin,
|
||||
type UpsertPluginConfig,
|
||||
type PatchPluginConfig,
|
||||
type UpsertPluginCompanySettings,
|
||||
type UpdateCompanyPluginAvailability,
|
||||
type ListCompanyPluginAvailability,
|
||||
type UpdatePluginStatus,
|
||||
type UninstallPlugin,
|
||||
type PluginStateScopeKey,
|
||||
type SetPluginState,
|
||||
type ListPluginState,
|
||||
} from "./plugin.js";
|
||||
|
||||
694
packages/shared/src/validators/plugin.ts
Normal file
694
packages/shared/src/validators/plugin.ts
Normal file
@@ -0,0 +1,694 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
PLUGIN_STATUSES,
|
||||
PLUGIN_CATEGORIES,
|
||||
PLUGIN_CAPABILITIES,
|
||||
PLUGIN_UI_SLOT_TYPES,
|
||||
PLUGIN_UI_SLOT_ENTITY_TYPES,
|
||||
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, and comma-separated lists.
|
||||
*/
|
||||
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(),
|
||||
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"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Company plugin availability / settings schemas
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Schema for creating or replacing company-scoped plugin settings.
|
||||
*
|
||||
* Company-specific settings are stored separately from instance-level
|
||||
* `plugin_config`, allowing the host to expose a company availability toggle
|
||||
* without changing the global install state of the plugin.
|
||||
*/
|
||||
export const upsertPluginCompanySettingsSchema = z.object({
|
||||
settingsJson: z.record(z.unknown()).optional().default({}),
|
||||
lastError: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpsertPluginCompanySettings = z.infer<typeof upsertPluginCompanySettingsSchema>;
|
||||
|
||||
/**
|
||||
* Schema for mutating a plugin's availability for a specific company.
|
||||
*
|
||||
* `available=false` lets callers disable access without uninstalling the
|
||||
* plugin globally. Optional `settingsJson` supports carrying company-specific
|
||||
* configuration alongside the availability update.
|
||||
*/
|
||||
export const updateCompanyPluginAvailabilitySchema = z.object({
|
||||
available: z.boolean(),
|
||||
settingsJson: z.record(z.unknown()).optional(),
|
||||
lastError: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpdateCompanyPluginAvailability = z.infer<typeof updateCompanyPluginAvailabilitySchema>;
|
||||
|
||||
/**
|
||||
* Query schema for company plugin availability list endpoints.
|
||||
*/
|
||||
export const listCompanyPluginAvailabilitySchema = z.object({
|
||||
available: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type ListCompanyPluginAvailability = z.infer<typeof listCompanyPluginAvailabilitySchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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>;
|
||||
Reference in New Issue
Block a user