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:
DottaandPaperclip authored and GitHub committed 2026-10-30 19:21:33 +00:00
1 parent 36f4422389
commit 8f822bfcf7
57 files changed
+3670 -250

No files matched your search

+21 -17
View File
@@ -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];
}