mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 23:43:16 +01:00
fix(cloud): persist configuration and load release-image packages (#18335)
Land the approved Cloud configuration and release-image package stack.
Includes the separately reviewed adapter settings (#18335), external adapters (#18333), bundled plugins (#18336), startup configuration (#18339), and migration/package checks (#18341).
Includes the final image-owned loader and linked-dependency inventory corrections.
Verified stack head: 462716170d.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
36f4422389
commit
8f822bfcf7
57 files changed
+3670
-250
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { resolveBundledPluginRuntimeLoader } from "./bundled-plugin-runtime.js";
|
||||
import { finishServiceShutdown } from "../shutdown.js";
|
||||
/**
|
||||
* PluginLoader — discovery, installation, and runtime activation of plugins.
|
||||
@@ -31,7 +32,7 @@ import { execFile } from "node:child_process";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { isDeepStrictEqual, promisify } from "node:util";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { PLUGIN_RPC_ERROR_CODES } from "@paperclipai/plugin-sdk";
|
||||
import type {
|
||||
@@ -271,6 +272,8 @@ function getDeclaredPageRoutePaths(manifest: PaperclipPluginManifestV1): string[
|
||||
* Options for the plugin loader service.
|
||||
*/
|
||||
export interface PluginLoaderOptions {
|
||||
resolvePackageRoot?: (plugin: PluginRecord) => string;
|
||||
prebuiltOnly?: boolean;
|
||||
/** Image-owned deployment policy, checked before importing code and starting workers. */
|
||||
assertPackageActivation?: (input: {
|
||||
pluginKey?: string;
|
||||
@@ -1157,6 +1160,8 @@ export function pluginLoader(
|
||||
enableLocalFilesystem = true,
|
||||
enableNpmDiscovery = true,
|
||||
assertPackageActivation,
|
||||
resolvePackageRoot,
|
||||
prebuiltOnly = false,
|
||||
} = options;
|
||||
|
||||
const registry = pluginRegistryService(db);
|
||||
@@ -1211,6 +1216,7 @@ export function pluginLoader(
|
||||
installOptions: PluginInstallOptions,
|
||||
): Promise<DiscoveredPlugin> {
|
||||
const { packageName, localPath, version, installDir } = installOptions;
|
||||
if (prebuiltOnly && !localPath) throw new Error("Managed plugins must come from the current image catalog");
|
||||
|
||||
if (!packageName && !localPath) {
|
||||
throw new Error("Either packageName or localPath must be provided");
|
||||
@@ -1286,7 +1292,7 @@ export function pluginLoader(
|
||||
const pkgJson = await readPackageJson(resolvedPackagePath);
|
||||
if (!pkgJson) throw new Error(`Missing package.json at ${resolvedPackagePath}`);
|
||||
|
||||
if (localPath) {
|
||||
if (localPath && !prebuiltOnly) {
|
||||
await ensureLocalPluginBuilt(resolvedPackagePath, pkgJson);
|
||||
}
|
||||
|
||||
@@ -1398,7 +1404,7 @@ export function pluginLoader(
|
||||
}
|
||||
|
||||
assertPackageActivation?.({ packageRoot, installedPackagePath: plugin.packagePath, pluginKey: plugin.pluginKey, manifest, previousManifest: plugin.manifestJson });
|
||||
if (JSON.stringify(manifest) === JSON.stringify(plugin.manifestJson)) {
|
||||
if (isDeepStrictEqual(manifest, plugin.manifestJson)) {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@@ -1749,6 +1755,7 @@ export function pluginLoader(
|
||||
packagePath: discovered.source === "local-filesystem" ? discovered.packagePath : undefined,
|
||||
},
|
||||
manifest,
|
||||
{ idempotent: prebuiltOnly },
|
||||
);
|
||||
|
||||
if (!installed) {
|
||||
@@ -1803,12 +1810,7 @@ export function pluginLoader(
|
||||
newManifest: PaperclipPluginManifestV1;
|
||||
discovered: DiscoveredPlugin;
|
||||
}> {
|
||||
const plugin = (await registry.getById(pluginId)) as {
|
||||
id: string;
|
||||
packageName: string;
|
||||
packagePath: string | null;
|
||||
manifestJson: PaperclipPluginManifestV1;
|
||||
} | null;
|
||||
const plugin = await registry.getById(pluginId);
|
||||
if (!plugin) throw new Error(`Plugin not found: ${pluginId}`);
|
||||
|
||||
const oldManifest = plugin.manifestJson;
|
||||
@@ -1817,7 +1819,7 @@ export function pluginLoader(
|
||||
// For local-path installs, fall back to the stored packagePath so
|
||||
// `upgradePlugin` can re-read the manifest from disk without needing
|
||||
// the caller to re-supply the path every time.
|
||||
localPath = plugin.packagePath ?? undefined,
|
||||
localPath = resolvePackageRoot ? resolvePackageRoot(plugin) : plugin.packagePath ?? undefined,
|
||||
version,
|
||||
} = upgradeOptions;
|
||||
|
||||
@@ -1887,6 +1889,7 @@ export function pluginLoader(
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
async cleanupInstallArtifacts(plugin: PluginRecord): Promise<void> {
|
||||
if (prebuiltOnly) return;
|
||||
const managedTargets = new Set<string>();
|
||||
const managedNodeModulesDir = resolveManagedInstallPackageDir(localPluginDir, plugin.packageName);
|
||||
const directManagedDir = path.join(localPluginDir, plugin.packageName);
|
||||
@@ -2262,12 +2265,14 @@ export function pluginLoader(
|
||||
// ------------------------------------------------------------------
|
||||
// 1. Resolve worker entrypoint
|
||||
// ------------------------------------------------------------------
|
||||
const packageRoot = resolvePluginPackageRoot(activePlugin, localPluginDir);
|
||||
const packageRoot = resolvePackageRoot ? resolvePackageRoot(activePlugin) : resolvePluginPackageRoot(activePlugin, localPluginDir);
|
||||
assertPackageActivation?.({ pluginKey, packageRoot, installedPackagePath: activePlugin.packagePath });
|
||||
activePlugin = await refreshPluginManifestFromPackage(activePlugin, packageRoot);
|
||||
manifest = activePlugin.manifestJson;
|
||||
assertPackageActivation?.({ pluginKey, packageRoot, installedPackagePath: activePlugin.packagePath, manifest });
|
||||
const workerEntrypoint = resolveWorkerEntrypoint(activePlugin, localPluginDir);
|
||||
const workerEntrypoint = resolvePackageRoot
|
||||
? path.resolve(packageRoot, manifest.entrypoints.worker)
|
||||
: resolveWorkerEntrypoint(activePlugin, localPluginDir);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2. Apply restricted database migrations before worker startup
|
||||
@@ -2334,7 +2339,7 @@ export function pluginLoader(
|
||||
env: buildPluginWorkerEnv({
|
||||
manifest,
|
||||
packageName: activePlugin.packageName,
|
||||
packagePath: activePlugin.packagePath,
|
||||
packagePath: resolvePackageRoot ? packageRoot : activePlugin.packagePath,
|
||||
instanceInfo,
|
||||
}),
|
||||
// Authorize the worker to act on each configured company from its
|
||||
@@ -2345,10 +2350,9 @@ export function pluginLoader(
|
||||
proactiveCompanyScopes: configRows.map((row) => row.companyId),
|
||||
};
|
||||
|
||||
// Repo-local plugin installs can resolve workspace TS sources at runtime
|
||||
// (for example @paperclipai/shared exports). Run those workers through
|
||||
// the tsx loader so first-party example plugins work in development.
|
||||
if (activePlugin.packagePath && existsSync(DEV_TSX_LOADER_PATH)) {
|
||||
if (prebuiltOnly) {
|
||||
workerOptions.execArgv = ["--import", resolveBundledPluginRuntimeLoader()];
|
||||
} else if (activePlugin.packagePath && existsSync(DEV_TSX_LOADER_PATH)) {
|
||||
workerOptions.execArgv = ["--import", DEV_TSX_LOADER_PATH];
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user