mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
feat(plugins): load image-owned catalogs and persistent app overlays
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
64895f187b
commit
be9c775d9e
17 files changed
+470
-2
No files matched your search
@@ -0,0 +1,79 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync, realpathSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/shared";
|
||||
import { distributionBundleDigest, distributionPluginActivationGuard, readDistributionPluginCatalog } from "../services/distribution-plugin-catalog.js";
|
||||
import { BUNDLED_PLUGIN_CATALOG, resolveBundledPluginInstalls } from "../services/bundled-plugins.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
function fixture() {
|
||||
const root = realpathSync(mkdtempSync(path.join(os.tmpdir(), "distribution-plugin-"))); roots.push(root);
|
||||
const localPath = path.join(root, "distribution", "acme.widget");
|
||||
mkdirSync(path.join(localPath, "dist", "ui"), { recursive: true });
|
||||
writeFileSync(path.join(localPath, "package.json"), JSON.stringify({ name: "@acme/plugin-widget", version: "1.0.0", paperclipPlugin: { manifest: "./dist/manifest.js", worker: "./dist/worker.js", ui: "./dist/ui/" } }));
|
||||
writeFileSync(path.join(localPath, "dist", "manifest.js"), "export default {};");
|
||||
writeFileSync(path.join(localPath, "dist", "worker.js"), "export default {};");
|
||||
const entry = { key: "acme.widget", pluginKey: "acme.widget", version: "1.0.0", directory: "acme.widget", digest: distributionBundleDigest(localPath) };
|
||||
const save = (plugins: unknown[] = [entry]) => writeFileSync(path.join(root, "distribution", "catalog.json"), JSON.stringify({ schemaVersion: 1, plugins }));
|
||||
save(); return { root, localPath, entry, save };
|
||||
}
|
||||
describe("image-owned plugin catalogs", () => {
|
||||
it("preserves images without a distribution catalog", () => {
|
||||
expect(readDistributionPluginCatalog("/nonexistent/catalog", BUNDLED_PLUGIN_CATALOG)).toEqual([]);
|
||||
});
|
||||
it("resolves selected private bundles alongside built-ins after verifying their bytes", () => {
|
||||
const { root, localPath } = fixture();
|
||||
const installs = resolveBundledPluginInstalls(["daytona", "acme.widget"], { catalogRoot: root, env: {}, enforceCatalogRoot: true });
|
||||
expect(installs.map(({ key }) => key)).toEqual(["daytona", "acme.widget"]);
|
||||
expect(installs[1]?.localPath).toBe(localPath);
|
||||
});
|
||||
it("rejects tampered bundle bytes before importing manifest code", () => {
|
||||
const { root, localPath } = fixture();
|
||||
writeFileSync(path.join(localPath, "dist", "manifest.js"), "throw new Error('must never execute');");
|
||||
expect(() => readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG)).toThrow(/digest mismatch/);
|
||||
});
|
||||
it("rejects duplicate keys, plugin identities and built-in replacement", () => {
|
||||
const { root, entry, save } = fixture();
|
||||
for (const plugins of [[entry, entry], [{ ...entry, key: "daytona" }], [{ ...entry, pluginKey: "paperclip.daytona-sandbox-provider" }]]) {
|
||||
save(plugins);
|
||||
expect(() => readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG)).toThrow(/Duplicate or built-in/);
|
||||
}
|
||||
});
|
||||
it("rejects traversal, symlinked artifacts and unbuilt entrypoints", () => {
|
||||
const { root, entry, localPath, save } = fixture();
|
||||
save([{ ...entry, directory: "../elsewhere" }]);
|
||||
expect(() => readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG)).toThrow();
|
||||
save(); symlinkSync(os.tmpdir(), path.join(localPath, "outside"));
|
||||
expect(() => readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG)).toThrow(/symlinks/);
|
||||
rmSync(path.join(localPath, "outside")); rmSync(path.join(localPath, "dist", "worker.js"));
|
||||
save([{ ...entry, digest: distributionBundleDigest(localPath) }]);
|
||||
expect(() => readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG)).toThrow();
|
||||
});
|
||||
it("blocks a persisted plugin after deselection/removal while preserving ordinary plugins", () => {
|
||||
const { root, localPath } = fixture();
|
||||
const entries = readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG);
|
||||
const input = { pluginKey: "acme.widget", packageRoot: localPath };
|
||||
expect(() => distributionPluginActivationGuard(root, entries, ["acme.widget"])(input)).not.toThrow();
|
||||
expect(() => distributionPluginActivationGuard(root, entries, ["acme.widget"])({ packageRoot: localPath })).not.toThrow();
|
||||
expect(() => distributionPluginActivationGuard(root, entries, [])({ packageRoot: localPath })).toThrow(/not selected/);
|
||||
expect(() => distributionPluginActivationGuard(root, entries, [])(input)).toThrow(/not selected/);
|
||||
expect(() => distributionPluginActivationGuard(root, [], [])(input)).toThrow(/absent/);
|
||||
expect(() => distributionPluginActivationGuard(root, [], [])({ pluginKey: "ordinary.plugin", packageRoot: path.join(root, "ordinary") })).not.toThrow();
|
||||
});
|
||||
it("rejects dangling catalog symlinks instead of treating them as absent", () => {
|
||||
const { root } = fixture();
|
||||
const file = path.join(root, "distribution", "catalog.json");
|
||||
rmSync(file); symlinkSync(path.join(root, "missing"), file);
|
||||
expect(() => readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG)).toThrow(/Invalid distribution catalog/);
|
||||
});
|
||||
it("requires catalog identity and version before activating imported manifests", () => {
|
||||
const { root, localPath } = fixture();
|
||||
const entries = readDistributionPluginCatalog(root, BUNDLED_PLUGIN_CATALOG);
|
||||
const guard = distributionPluginActivationGuard(root, entries, ["acme.widget"]);
|
||||
for (const manifest of [{ id: "other.plugin", version: "1.0.0" }, { id: "acme.widget", version: "2.0.0" }]) {
|
||||
expect(() => guard({ pluginKey: "acme.widget", packageRoot: localPath, manifest: manifest as PaperclipPluginManifestV1 })).toThrow(/identity\/version/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -132,10 +132,12 @@ import {
|
||||
} from "./services/plugin-loader.js";
|
||||
import {
|
||||
SELF_HOSTED_AUTO_INSTALL_KEYS,
|
||||
BUNDLED_PLUGIN_CATALOG,
|
||||
ensureBundledPlugins,
|
||||
resolveBundledCatalogRoot,
|
||||
resolveBundledPluginInstalls,
|
||||
} from "./services/bundled-plugins.js";
|
||||
import { readDistributionPluginCatalog, distributionPluginActivationGuard } from "./services/distribution-plugin-catalog.js";
|
||||
import {
|
||||
createPluginWorkerManager,
|
||||
type PluginWorkerManager,
|
||||
@@ -596,12 +598,14 @@ export async function createApp(
|
||||
const managedAutoInstallKeys = opts.managedPluginAutoInstall ?? null;
|
||||
const bundledCatalogRoot =
|
||||
opts.bundledPluginCatalogRoot ?? resolveBundledCatalogRoot(process.env);
|
||||
const distributionPlugins = readDistributionPluginCatalog(bundledCatalogRoot, BUNDLED_PLUGIN_CATALOG);
|
||||
const bundledPluginInstalls = resolveBundledPluginInstalls(
|
||||
managedAutoInstallKeys ?? SELF_HOSTED_AUTO_INSTALL_KEYS,
|
||||
{
|
||||
catalogRoot: bundledCatalogRoot,
|
||||
env: process.env,
|
||||
enforceCatalogRoot: managedAutoInstallKeys !== null,
|
||||
distributionPlugins,
|
||||
},
|
||||
);
|
||||
const managedBundledPluginKeys =
|
||||
@@ -869,6 +873,7 @@ export async function createApp(
|
||||
{
|
||||
localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR,
|
||||
migrationDb: opts.pluginMigrationDb,
|
||||
assertPackageActivation: distributionPluginActivationGuard(bundledCatalogRoot, distributionPlugins, managedAutoInstallKeys),
|
||||
},
|
||||
{
|
||||
workerManager,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/shared";
|
||||
import { readDistributionPluginCatalog, type DistributionPlugin } from "./distribution-plugin-catalog.js";
|
||||
|
||||
/**
|
||||
* Bundled plugin auto-provisioning.
|
||||
@@ -123,6 +124,8 @@ export interface ResolvedBundledPlugin {
|
||||
pluginKey: string;
|
||||
/** Absolute path handed to `loader.installPlugin({ localPath })`. */
|
||||
localPath: string;
|
||||
/** Image-owned entries require exact manifest identity/version matching. */
|
||||
distribution?: DistributionPlugin;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,16 +164,23 @@ export function resolveBundledPluginInstalls(
|
||||
catalogRoot: string;
|
||||
env: Record<string, string | undefined>;
|
||||
enforceCatalogRoot: boolean;
|
||||
distributionPlugins?: readonly DistributionPlugin[];
|
||||
},
|
||||
): ResolvedBundledPlugin[] {
|
||||
const resolved: ResolvedBundledPlugin[] = [];
|
||||
const seen = new Set<string>();
|
||||
const canonicalRoot = canonicalize(opts.catalogRoot);
|
||||
const distributionPlugins = opts.distributionPlugins ?? readDistributionPluginCatalog(opts.catalogRoot, BUNDLED_PLUGIN_CATALOG);
|
||||
for (const key of keys) {
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const entry = BUNDLED_PLUGIN_CATALOG.find((candidate) => candidate.key === key);
|
||||
if (!entry) {
|
||||
const distribution = distributionPlugins.find((candidate) => candidate.key === key);
|
||||
if (distribution) {
|
||||
resolved.push({ key, pluginKey: distribution.pluginKey, localPath: distribution.localPath, distribution });
|
||||
continue;
|
||||
}
|
||||
const known = BUNDLED_PLUGIN_CATALOG.map((candidate) => candidate.key).join(", ");
|
||||
throw new Error(
|
||||
`bundled plugin auto-install key "${key}" is not in the bundled catalog (known keys: ${known}); refusing to start`,
|
||||
@@ -357,6 +367,12 @@ export async function ensureBundledPlugins(
|
||||
const bundleManifestExists = deps.bundleManifestExists ?? defaultBundleManifestExists;
|
||||
for (const install of installs) {
|
||||
try {
|
||||
if (install.distribution) {
|
||||
const manifest = await deps.loader.loadManifest(install.localPath);
|
||||
if (manifest?.id !== install.pluginKey || manifest.version !== install.distribution.version) {
|
||||
throw new Error("Distribution manifest does not match its catalog identity/version");
|
||||
}
|
||||
}
|
||||
const existing = await deps.registry.getByKey(install.pluginKey);
|
||||
if (existing && (existing.status !== "uninstalled" || !opts.reinstallUninstalled)) {
|
||||
// The bundle ships with the release image, so its manifest is the
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/shared";
|
||||
|
||||
const segment = z.string().regex(/^[a-z][a-z0-9.-]{0,99}$/);
|
||||
export const distributionPluginCatalogSchema = z.object({
|
||||
schemaVersion: z.literal(1),
|
||||
plugins: z.array(z.object({
|
||||
key: segment,
|
||||
pluginKey: segment,
|
||||
version: z.string().regex(/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/),
|
||||
directory: segment,
|
||||
digest: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
||||
}).strict()).max(100),
|
||||
}).strict();
|
||||
|
||||
export type DistributionPlugin = z.infer<typeof distributionPluginCatalogSchema>["plugins"][number] & {
|
||||
localPath: string;
|
||||
};
|
||||
|
||||
export function distributionPluginsRoot(catalogRoot: string): string {
|
||||
// Match canonical paths persisted by local-path installs (for example,
|
||||
// macOS /tmp -> /private/tmp), without permitting a symlinked catalog itself.
|
||||
try { return path.join(fs.realpathSync(catalogRoot), "distribution"); }
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
return path.resolve(catalogRoot, "distribution");
|
||||
}
|
||||
}
|
||||
|
||||
/** Guard all activation paths, including persisted installs after a rollback. */
|
||||
export function distributionPluginActivationGuard(
|
||||
catalogRoot: string,
|
||||
entries: readonly DistributionPlugin[],
|
||||
selectedKeys: readonly string[] | null,
|
||||
) {
|
||||
const root = distributionPluginsRoot(catalogRoot);
|
||||
return (input: { pluginKey?: string; packageRoot: string; manifest?: PaperclipPluginManifestV1 }) => {
|
||||
let packageRoot: string;
|
||||
try { packageRoot = fs.realpathSync(input.packageRoot); }
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
packageRoot = path.resolve(input.packageRoot);
|
||||
}
|
||||
const entry = entries.find((candidate) => input.pluginKey ? candidate.pluginKey === input.pluginKey : candidate.localPath === packageRoot);
|
||||
const relative = path.relative(root, packageRoot);
|
||||
const inside = relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
if (!inside && !entry) return;
|
||||
if (!entry || (selectedKeys !== null && !selectedKeys.includes(entry.key)) || packageRoot !== entry.localPath) {
|
||||
throw new Error("Distribution plugin is absent or not selected in this deployment");
|
||||
}
|
||||
if (input.manifest && (input.manifest.id !== entry.pluginKey || input.manifest.version !== entry.version)) {
|
||||
throw new Error("Distribution manifest does not match its catalog identity/version");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Same portable file inventory used by image builders: path, SHA-256, mode. */
|
||||
export function distributionBundleDigest(root: string): string {
|
||||
const inventory: Array<[string, string, number]> = [];
|
||||
let bytesRead = 0;
|
||||
const hash = (value: Buffer | string) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
||||
function walk(directory: string, relative = "") {
|
||||
if (relative.split("/").length > 32) throw new Error("Distribution bundle exceeds directory depth limit");
|
||||
for (const name of fs.readdirSync(directory).sort()) {
|
||||
const relativePath = relative ? `${relative}/${name}` : name;
|
||||
const file = path.join(directory, name);
|
||||
const stat = fs.lstatSync(file);
|
||||
if (stat.isSymbolicLink()) throw new Error("Distribution bundles cannot contain symlinks");
|
||||
if (stat.isDirectory()) walk(file, relativePath);
|
||||
else if (stat.isFile()) {
|
||||
bytesRead += stat.size;
|
||||
if (bytesRead > 256 * 1024 * 1024 || inventory.length >= 10_000) throw new Error("Distribution bundle exceeds verification limits");
|
||||
inventory.push([relativePath, hash(fs.readFileSync(file)), stat.mode & 0o777]);
|
||||
} else throw new Error("Distribution bundles contain only regular files and directories");
|
||||
}
|
||||
}
|
||||
walk(root);
|
||||
if (!inventory.length) throw new Error("Distribution bundle is empty");
|
||||
return hash(JSON.stringify(inventory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional, image-owned extension catalog. No URLs, executable configuration,
|
||||
* runtime dependency installation, or replacement of a built-in entry.
|
||||
* Validate bytes before importing any manifest code.
|
||||
*/
|
||||
export function readDistributionPluginCatalog(
|
||||
catalogRoot: string,
|
||||
builtins: readonly { key: string; pluginKey: string }[],
|
||||
): DistributionPlugin[] {
|
||||
const root = distributionPluginsRoot(catalogRoot);
|
||||
const file = path.join(root, "catalog.json");
|
||||
const rootStat = fs.lstatSync(root, { throwIfNoEntry: false });
|
||||
if (!rootStat) return [];
|
||||
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) throw new Error("Distribution catalog root must be a regular directory, not a symlink");
|
||||
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
||||
if (!stat) return [];
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) throw new Error("Invalid distribution catalog file");
|
||||
const catalog = distributionPluginCatalogSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
|
||||
const keys = new Set(builtins.map((entry) => entry.key));
|
||||
const pluginKeys = new Set(builtins.map((entry) => entry.pluginKey));
|
||||
const directories = new Set<string>();
|
||||
return catalog.plugins.map((entry) => {
|
||||
if (keys.has(entry.key) || pluginKeys.has(entry.pluginKey) || directories.has(entry.directory)) {
|
||||
throw new Error("Duplicate or built-in distribution plugin identity");
|
||||
}
|
||||
keys.add(entry.key); pluginKeys.add(entry.pluginKey); directories.add(entry.directory);
|
||||
const localPath = path.join(root, entry.directory);
|
||||
const info = fs.lstatSync(localPath);
|
||||
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Invalid distribution bundle directory");
|
||||
if (distributionBundleDigest(localPath) !== entry.digest) throw new Error(`Distribution bundle digest mismatch: ${entry.key}`);
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(localPath, "package.json"), "utf8"));
|
||||
if (pkg.version !== entry.version || !pkg.paperclipPlugin) throw new Error("Distribution package version or entrypoints missing");
|
||||
for (const name of ["manifest", "worker", "ui"] as const) {
|
||||
const declared = pkg.paperclipPlugin[name];
|
||||
if (name === "ui" && declared === undefined) continue;
|
||||
const relative = typeof declared === "string" ? declared.replace(/^\.\//, "").replace(/\/$/, "") : "";
|
||||
if (!relative || relative.startsWith("/") || relative.includes("\\") || relative.split("/").some((part: string) => !part || part === "." || part === "..")) {
|
||||
throw new Error("Distribution entrypoint must stay inside its bundle");
|
||||
}
|
||||
const target = fs.statSync(path.join(localPath, relative));
|
||||
if (name === "ui" ? !target.isDirectory() : !target.isFile()) throw new Error("Distribution entrypoint is not prebuilt");
|
||||
}
|
||||
return { ...entry, localPath };
|
||||
});
|
||||
}
|
||||
@@ -163,6 +163,7 @@ const UI_SLOT_CAPABILITIES: Record<PluginUiSlotType, PluginCapability> = {
|
||||
taskDetailView: "ui.detailTab.register",
|
||||
dashboardWidget: "ui.dashboardWidget.register",
|
||||
globalToolbarButton: "ui.action.register",
|
||||
appShellOverlay: "ui.action.register",
|
||||
toolbarButton: "ui.action.register",
|
||||
contextMenuItem: "ui.action.register",
|
||||
commentAnnotation: "ui.commentAnnotation.register",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import { realpath, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { BUNDLED_LOCAL_PLUGIN_ROOT } from "./plugin-loader.js";
|
||||
import { BUNDLED_CATALOG_ROOT_ENV_VAR } from "./bundled-plugins.js";
|
||||
|
||||
export { isCloudManagedInstance } from "./cloud-instance.js";
|
||||
|
||||
@@ -93,7 +94,7 @@ export async function isWithinBundledPluginRoot(
|
||||
canonicalPath: string,
|
||||
bundledRootOverride?: string,
|
||||
): Promise<boolean> {
|
||||
const bundledRoot = bundledRootOverride ?? BUNDLED_LOCAL_PLUGIN_ROOT;
|
||||
const bundledRoot = bundledRootOverride ?? (process.env[BUNDLED_CATALOG_ROOT_ENV_VAR]?.trim() || BUNDLED_LOCAL_PLUGIN_ROOT);
|
||||
|
||||
let canonicalRoot: string;
|
||||
try {
|
||||
|
||||
@@ -269,6 +269,12 @@ function getDeclaredPageRoutePaths(manifest: PaperclipPluginManifestV1): string[
|
||||
* Options for the plugin loader service.
|
||||
*/
|
||||
export interface PluginLoaderOptions {
|
||||
/** Image-owned deployment policy, checked before importing code and starting workers. */
|
||||
assertPackageActivation?: (input: {
|
||||
pluginKey?: string;
|
||||
packageRoot: string;
|
||||
manifest?: PaperclipPluginManifestV1;
|
||||
}) => void;
|
||||
/**
|
||||
* Path to the local plugin directory to scan.
|
||||
* Defaults to ~/.paperclip/plugins/
|
||||
@@ -1144,6 +1150,7 @@ export function pluginLoader(
|
||||
migrationDb = db,
|
||||
enableLocalFilesystem = true,
|
||||
enableNpmDiscovery = true,
|
||||
assertPackageActivation,
|
||||
} = options;
|
||||
|
||||
const registry = pluginRegistryService(db);
|
||||
@@ -1269,6 +1276,7 @@ export function pluginLoader(
|
||||
|
||||
// Step 3: Read and validate plugin manifest
|
||||
// Note: this.loadManifest (used via current context)
|
||||
assertPackageActivation?.({ packageRoot: resolvedPackagePath });
|
||||
const pkgJson = await readPackageJson(resolvedPackagePath);
|
||||
if (!pkgJson) throw new Error(`Missing package.json at ${resolvedPackagePath}`);
|
||||
|
||||
@@ -1287,6 +1295,7 @@ export function pluginLoader(
|
||||
}
|
||||
|
||||
const manifest = await loadManifestFromPath(manifestPath);
|
||||
assertPackageActivation?.({ packageRoot: resolvedPackagePath, pluginKey: manifest.id, manifest });
|
||||
|
||||
// Step 4: Reject incompatible plugin API versions
|
||||
if (!manifestValidator.getSupportedVersions().includes(manifest.apiVersion)) {
|
||||
@@ -1382,6 +1391,7 @@ export function pluginLoader(
|
||||
);
|
||||
}
|
||||
|
||||
assertPackageActivation?.({ packageRoot, pluginKey: plugin.pluginKey, manifest });
|
||||
if (JSON.stringify(manifest) === JSON.stringify(plugin.manifestJson)) {
|
||||
return plugin;
|
||||
}
|
||||
@@ -1409,6 +1419,7 @@ export function pluginLoader(
|
||||
packagePath: string,
|
||||
source: PluginSource,
|
||||
): Promise<DiscoveredPlugin | null> {
|
||||
assertPackageActivation?.({ packageRoot: packagePath });
|
||||
const pkgJson = await readPackageJson(packagePath);
|
||||
if (!pkgJson) return null;
|
||||
|
||||
@@ -1438,6 +1449,7 @@ export function pluginLoader(
|
||||
|
||||
try {
|
||||
const manifest = await loadManifestFromPath(manifestPath);
|
||||
assertPackageActivation?.({ packageRoot: packagePath, pluginKey: manifest.id, manifest });
|
||||
return {
|
||||
packagePath,
|
||||
packageName,
|
||||
@@ -1690,6 +1702,7 @@ export function pluginLoader(
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
async loadManifest(packagePath: string): Promise<PaperclipPluginManifestV1 | null> {
|
||||
assertPackageActivation?.({ packageRoot: packagePath });
|
||||
const pkgJson = await readPackageJson(packagePath);
|
||||
if (!pkgJson) return null;
|
||||
|
||||
@@ -1704,7 +1717,9 @@ export function pluginLoader(
|
||||
const manifestPath = resolveManifestPath(packagePath, pkgJson);
|
||||
if (!manifestPath || !existsSync(manifestPath)) return null;
|
||||
|
||||
return loadManifestFromPath(manifestPath);
|
||||
const manifest = await loadManifestFromPath(manifestPath);
|
||||
assertPackageActivation?.({ packageRoot: packagePath, pluginKey: manifest.id, manifest });
|
||||
return manifest;
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -2246,8 +2261,10 @@ export function pluginLoader(
|
||||
// 1. Resolve worker entrypoint
|
||||
// ------------------------------------------------------------------
|
||||
const packageRoot = resolvePluginPackageRoot(activePlugin, localPluginDir);
|
||||
assertPackageActivation?.({ pluginKey, packageRoot });
|
||||
activePlugin = await refreshPluginManifestFromPackage(activePlugin, packageRoot);
|
||||
manifest = activePlugin.manifestJson;
|
||||
assertPackageActivation?.({ pluginKey, packageRoot, manifest });
|
||||
const workerEntrypoint = resolveWorkerEntrypoint(activePlugin, localPluginDir);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user