2026-03-13 16:22:34 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* PluginCapabilityValidator — enforces the capability model at both
|
|
|
|
|
|
* install-time and runtime.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Every plugin declares the capabilities it requires in its manifest
|
|
|
|
|
|
* (`manifest.capabilities`). This service checks those declarations
|
|
|
|
|
|
* against a mapping of operations → required capabilities so that:
|
|
|
|
|
|
*
|
|
|
|
|
|
* 1. **Install-time validation** — `validateManifestCapabilities()`
|
|
|
|
|
|
* ensures that declared features (tools, jobs, webhooks, UI slots)
|
|
|
|
|
|
* have matching capability entries, giving operators clear feedback
|
|
|
|
|
|
* before a plugin is activated.
|
|
|
|
|
|
*
|
|
|
|
|
|
* 2. **Runtime gating** — `checkOperation()` / `assertOperation()` are
|
|
|
|
|
|
* called on every worker→host bridge call to enforce least-privilege
|
|
|
|
|
|
* access. If a plugin attempts an operation it did not declare, the
|
|
|
|
|
|
* call is rejected with a 403 error.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @see PLUGIN_SPEC.md §15 — Capability Model
|
|
|
|
|
|
* @see host-client-factory.ts — SDK-side capability gating
|
|
|
|
|
|
*/
|
|
|
|
|
|
import type {
|
|
|
|
|
|
PluginCapability,
|
|
|
|
|
|
PaperclipPluginManifestV1,
|
|
|
|
|
|
PluginUiSlotType,
|
|
|
|
|
|
PluginLauncherPlacementZone,
|
|
|
|
|
|
} from "@paperclipai/shared";
|
|
|
|
|
|
import { forbidden } from "../errors.js";
|
|
|
|
|
|
import { logger } from "../middleware/logger.js";
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Capability requirement mappings
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Maps high-level operations to the capabilities they require.
|
|
|
|
|
|
*
|
|
|
|
|
|
* When the bridge receives a call from a plugin worker, the host looks up
|
|
|
|
|
|
* the operation in this map and checks the plugin's declared capabilities.
|
|
|
|
|
|
* If any required capability is missing, the call is rejected.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @see PLUGIN_SPEC.md §15 — Capability Model
|
|
|
|
|
|
*/
|
|
|
|
|
|
const OPERATION_CAPABILITIES: Record<string, readonly PluginCapability[]> = {
|
|
|
|
|
|
// Data read operations
|
|
|
|
|
|
"companies.list": ["companies.read"],
|
|
|
|
|
|
"companies.get": ["companies.read"],
|
|
|
|
|
|
"projects.list": ["projects.read"],
|
|
|
|
|
|
"projects.get": ["projects.read"],
|
|
|
|
|
|
"project.workspaces.list": ["project.workspaces.read"],
|
|
|
|
|
|
"project.workspaces.get": ["project.workspaces.read"],
|
|
|
|
|
|
"issues.list": ["issues.read"],
|
|
|
|
|
|
"issues.get": ["issues.read"],
|
|
|
|
|
|
"issue.comments.list": ["issue.comments.read"],
|
|
|
|
|
|
"issue.comments.get": ["issue.comments.read"],
|
|
|
|
|
|
"agents.list": ["agents.read"],
|
|
|
|
|
|
"agents.get": ["agents.read"],
|
|
|
|
|
|
"goals.list": ["goals.read"],
|
|
|
|
|
|
"goals.get": ["goals.read"],
|
|
|
|
|
|
"activity.list": ["activity.read"],
|
|
|
|
|
|
"activity.get": ["activity.read"],
|
|
|
|
|
|
"costs.list": ["costs.read"],
|
|
|
|
|
|
"costs.get": ["costs.read"],
|
|
|
|
|
|
|
|
|
|
|
|
// Data write operations
|
|
|
|
|
|
"issues.create": ["issues.create"],
|
|
|
|
|
|
"issues.update": ["issues.update"],
|
|
|
|
|
|
"issue.comments.create": ["issue.comments.create"],
|
|
|
|
|
|
"activity.log": ["activity.log.write"],
|
|
|
|
|
|
"metrics.write": ["metrics.write"],
|
2026-03-31 13:18:50 -05:00
|
|
|
|
"telemetry.track": ["telemetry.track"],
|
2026-03-13 16:22:34 -05:00
|
|
|
|
|
|
|
|
|
|
// Plugin state operations
|
|
|
|
|
|
"plugin.state.get": ["plugin.state.read"],
|
|
|
|
|
|
"plugin.state.list": ["plugin.state.read"],
|
|
|
|
|
|
"plugin.state.set": ["plugin.state.write"],
|
|
|
|
|
|
"plugin.state.delete": ["plugin.state.write"],
|
|
|
|
|
|
|
|
|
|
|
|
// Runtime / Integration operations
|
|
|
|
|
|
"events.subscribe": ["events.subscribe"],
|
|
|
|
|
|
"events.emit": ["events.emit"],
|
|
|
|
|
|
"jobs.schedule": ["jobs.schedule"],
|
|
|
|
|
|
"jobs.cancel": ["jobs.schedule"],
|
|
|
|
|
|
"webhooks.receive": ["webhooks.receive"],
|
|
|
|
|
|
"http.request": ["http.outbound"],
|
|
|
|
|
|
"secrets.resolve": ["secrets.read-ref"],
|
|
|
|
|
|
|
|
|
|
|
|
// Agent tools
|
|
|
|
|
|
"agent.tools.register": ["agent.tools.register"],
|
|
|
|
|
|
"agent.tools.execute": ["agent.tools.register"],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Maps UI slot types to the capability required to register them.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @see PLUGIN_SPEC.md §19 — UI Extension Model
|
|
|
|
|
|
*/
|
|
|
|
|
|
const UI_SLOT_CAPABILITIES: Record<PluginUiSlotType, PluginCapability> = {
|
|
|
|
|
|
sidebar: "ui.sidebar.register",
|
|
|
|
|
|
sidebarPanel: "ui.sidebar.register",
|
|
|
|
|
|
projectSidebarItem: "ui.sidebar.register",
|
|
|
|
|
|
page: "ui.page.register",
|
|
|
|
|
|
detailTab: "ui.detailTab.register",
|
|
|
|
|
|
taskDetailView: "ui.detailTab.register",
|
|
|
|
|
|
dashboardWidget: "ui.dashboardWidget.register",
|
2026-03-14 15:05:04 -07:00
|
|
|
|
globalToolbarButton: "ui.action.register",
|
2026-03-13 16:22:34 -05:00
|
|
|
|
toolbarButton: "ui.action.register",
|
|
|
|
|
|
contextMenuItem: "ui.action.register",
|
|
|
|
|
|
commentAnnotation: "ui.commentAnnotation.register",
|
|
|
|
|
|
commentContextMenuItem: "ui.action.register",
|
|
|
|
|
|
settingsPage: "instance.settings.register",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Launcher placement zones align with host UI surfaces and therefore inherit
|
|
|
|
|
|
* the same capability requirements as the equivalent slot type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const LAUNCHER_PLACEMENT_CAPABILITIES: Record<
|
|
|
|
|
|
PluginLauncherPlacementZone,
|
|
|
|
|
|
PluginCapability
|
|
|
|
|
|
> = {
|
|
|
|
|
|
page: "ui.page.register",
|
|
|
|
|
|
detailTab: "ui.detailTab.register",
|
|
|
|
|
|
taskDetailView: "ui.detailTab.register",
|
|
|
|
|
|
dashboardWidget: "ui.dashboardWidget.register",
|
|
|
|
|
|
sidebar: "ui.sidebar.register",
|
|
|
|
|
|
sidebarPanel: "ui.sidebar.register",
|
|
|
|
|
|
projectSidebarItem: "ui.sidebar.register",
|
2026-03-14 15:05:04 -07:00
|
|
|
|
globalToolbarButton: "ui.action.register",
|
2026-03-13 16:22:34 -05:00
|
|
|
|
toolbarButton: "ui.action.register",
|
|
|
|
|
|
contextMenuItem: "ui.action.register",
|
|
|
|
|
|
commentAnnotation: "ui.commentAnnotation.register",
|
|
|
|
|
|
commentContextMenuItem: "ui.action.register",
|
|
|
|
|
|
settingsPage: "instance.settings.register",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Maps feature declarations in the manifest to their required capabilities.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const FEATURE_CAPABILITIES: Record<string, PluginCapability> = {
|
|
|
|
|
|
tools: "agent.tools.register",
|
|
|
|
|
|
jobs: "jobs.schedule",
|
|
|
|
|
|
webhooks: "webhooks.receive",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Result types
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Result of a capability check. When `allowed` is false, `missing` contains
|
|
|
|
|
|
* the capabilities that the plugin does not declare but the operation requires.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface CapabilityCheckResult {
|
|
|
|
|
|
allowed: boolean;
|
|
|
|
|
|
missing: PluginCapability[];
|
|
|
|
|
|
operation?: string;
|
|
|
|
|
|
pluginId?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// PluginCapabilityValidator interface
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export interface PluginCapabilityValidator {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin has a specific capability.
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasCapability(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capability: PluginCapability,
|
|
|
|
|
|
): boolean;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin has all of the specified capabilities.
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasAllCapabilities(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capabilities: PluginCapability[],
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin has at least one of the specified capabilities.
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasAnyCapability(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capabilities: PluginCapability[],
|
|
|
|
|
|
): boolean;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin is allowed to perform the named operation.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Operations are mapped to required capabilities via OPERATION_CAPABILITIES.
|
|
|
|
|
|
* Unknown operations are rejected by default.
|
|
|
|
|
|
*/
|
|
|
|
|
|
checkOperation(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
operation: string,
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Assert that a plugin is allowed to perform an operation.
|
|
|
|
|
|
* Throws a 403 HttpError if the capability check fails.
|
|
|
|
|
|
*/
|
|
|
|
|
|
assertOperation(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
operation: string,
|
|
|
|
|
|
): void;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Assert that a plugin has a specific capability.
|
|
|
|
|
|
* Throws a 403 HttpError if the capability is missing.
|
|
|
|
|
|
*/
|
|
|
|
|
|
assertCapability(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capability: PluginCapability,
|
|
|
|
|
|
): void;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin can register the given UI slot type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
checkUiSlot(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
slotType: PluginUiSlotType,
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Validate that a manifest's declared capabilities are consistent with its
|
|
|
|
|
|
* declared features (tools, jobs, webhooks, UI slots).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Returns all missing capabilities rather than failing on the first one.
|
|
|
|
|
|
* This is useful for install-time validation to give comprehensive feedback.
|
|
|
|
|
|
*/
|
|
|
|
|
|
validateManifestCapabilities(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get the capabilities required for a named operation.
|
|
|
|
|
|
* Returns an empty array if the operation is unknown.
|
|
|
|
|
|
*/
|
|
|
|
|
|
getRequiredCapabilities(operation: string): readonly PluginCapability[];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get the capability required for a UI slot type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
getUiSlotCapability(slotType: PluginUiSlotType): PluginCapability;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Factory
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create a PluginCapabilityValidator.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This service enforces capability gates for plugin operations. The host
|
|
|
|
|
|
* uses it to verify that a plugin's declared capabilities permit the
|
|
|
|
|
|
* operation it is attempting, both at install time (manifest validation)
|
|
|
|
|
|
* and at runtime (bridge call gating).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Usage:
|
|
|
|
|
|
* ```ts
|
|
|
|
|
|
* const validator = pluginCapabilityValidator();
|
|
|
|
|
|
*
|
|
|
|
|
|
* // Runtime: gate a bridge call
|
|
|
|
|
|
* validator.assertOperation(plugin.manifestJson, "issues.create");
|
|
|
|
|
|
*
|
|
|
|
|
|
* // Install time: validate manifest consistency
|
|
|
|
|
|
* const result = validator.validateManifestCapabilities(manifest);
|
|
|
|
|
|
* if (!result.allowed) {
|
|
|
|
|
|
* throw badRequest("Missing capabilities", result.missing);
|
|
|
|
|
|
* }
|
|
|
|
|
|
* ```
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function pluginCapabilityValidator(): PluginCapabilityValidator {
|
|
|
|
|
|
const log = logger.child({ service: "plugin-capability-validator" });
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// Internal helpers
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function capabilitySet(manifest: PaperclipPluginManifestV1): Set<PluginCapability> {
|
|
|
|
|
|
return new Set(manifest.capabilities);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildForbiddenMessage(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
operation: string,
|
|
|
|
|
|
missing: PluginCapability[],
|
|
|
|
|
|
): string {
|
|
|
|
|
|
return (
|
|
|
|
|
|
`Plugin '${manifest.id}' is not allowed to perform '${operation}'. ` +
|
|
|
|
|
|
`Missing required capabilities: ${missing.join(", ")}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// Public API
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
hasCapability(manifest, capability) {
|
|
|
|
|
|
return manifest.capabilities.includes(capability);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
hasAllCapabilities(manifest, capabilities) {
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
const missing = capabilities.filter((cap) => !declared.has(cap));
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: missing.length === 0,
|
|
|
|
|
|
missing,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
hasAnyCapability(manifest, capabilities) {
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
return capabilities.some((cap) => declared.has(cap));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
checkOperation(manifest, operation) {
|
|
|
|
|
|
const required = OPERATION_CAPABILITIES[operation];
|
|
|
|
|
|
|
|
|
|
|
|
if (!required) {
|
|
|
|
|
|
log.warn(
|
|
|
|
|
|
{ pluginId: manifest.id, operation },
|
|
|
|
|
|
"capability check for unknown operation – rejecting by default",
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: false,
|
|
|
|
|
|
missing: [],
|
|
|
|
|
|
operation,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
const missing = required.filter((cap) => !declared.has(cap));
|
|
|
|
|
|
|
|
|
|
|
|
if (missing.length > 0) {
|
|
|
|
|
|
log.debug(
|
|
|
|
|
|
{ pluginId: manifest.id, operation, missing },
|
|
|
|
|
|
"capability check failed",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: missing.length === 0,
|
|
|
|
|
|
missing,
|
|
|
|
|
|
operation,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
assertOperation(manifest, operation) {
|
|
|
|
|
|
const result = this.checkOperation(manifest, operation);
|
|
|
|
|
|
if (!result.allowed) {
|
|
|
|
|
|
const msg = result.missing.length > 0
|
|
|
|
|
|
? buildForbiddenMessage(manifest, operation, result.missing)
|
|
|
|
|
|
: `Plugin '${manifest.id}' attempted unknown operation '${operation}'`;
|
|
|
|
|
|
throw forbidden(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
assertCapability(manifest, capability) {
|
|
|
|
|
|
if (!this.hasCapability(manifest, capability)) {
|
|
|
|
|
|
throw forbidden(
|
|
|
|
|
|
`Plugin '${manifest.id}' lacks required capability '${capability}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
checkUiSlot(manifest, slotType) {
|
|
|
|
|
|
const required = UI_SLOT_CAPABILITIES[slotType];
|
|
|
|
|
|
if (!required) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: false,
|
|
|
|
|
|
missing: [],
|
|
|
|
|
|
operation: `ui.${slotType}.register`,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const has = manifest.capabilities.includes(required);
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: has,
|
|
|
|
|
|
missing: has ? [] : [required],
|
|
|
|
|
|
operation: `ui.${slotType}.register`,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
validateManifestCapabilities(manifest) {
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
const allMissing: PluginCapability[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Check feature declarations → required capabilities
|
|
|
|
|
|
for (const [feature, requiredCap] of Object.entries(FEATURE_CAPABILITIES)) {
|
|
|
|
|
|
const featureValue = manifest[feature as keyof PaperclipPluginManifestV1];
|
|
|
|
|
|
if (Array.isArray(featureValue) && featureValue.length > 0) {
|
|
|
|
|
|
if (!declared.has(requiredCap)) {
|
|
|
|
|
|
allMissing.push(requiredCap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check UI slots → required capabilities
|
|
|
|
|
|
const uiSlots = manifest.ui?.slots ?? [];
|
|
|
|
|
|
if (uiSlots.length > 0) {
|
|
|
|
|
|
for (const slot of uiSlots) {
|
|
|
|
|
|
const requiredCap = UI_SLOT_CAPABILITIES[slot.type];
|
|
|
|
|
|
if (requiredCap && !declared.has(requiredCap)) {
|
|
|
|
|
|
if (!allMissing.includes(requiredCap)) {
|
|
|
|
|
|
allMissing.push(requiredCap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check launcher declarations → required capabilities
|
|
|
|
|
|
const launchers = [
|
|
|
|
|
|
...(manifest.launchers ?? []),
|
|
|
|
|
|
...(manifest.ui?.launchers ?? []),
|
|
|
|
|
|
];
|
|
|
|
|
|
if (launchers.length > 0) {
|
|
|
|
|
|
for (const launcher of launchers) {
|
|
|
|
|
|
const requiredCap = LAUNCHER_PLACEMENT_CAPABILITIES[launcher.placementZone];
|
|
|
|
|
|
if (requiredCap && !declared.has(requiredCap) && !allMissing.includes(requiredCap)) {
|
|
|
|
|
|
allMissing.push(requiredCap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: allMissing.length === 0,
|
|
|
|
|
|
missing: allMissing,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getRequiredCapabilities(operation) {
|
|
|
|
|
|
return OPERATION_CAPABILITIES[operation] ?? [];
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getUiSlotCapability(slotType) {
|
|
|
|
|
|
return UI_SLOT_CAPABILITIES[slotType];
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|