mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
fix(cloud): honor managed plugin floors and provider origins
Align shipped provider package versions with their manifests and preserve the registry origin of self-hosted npm workers. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e645a9ebc4
commit
cd328c1e51
9 files changed
+85
-13
No files matched your search
@@ -37,7 +37,10 @@ In practice, that means the current implementation is a good fit for local devel
|
||||
### Managed image packages
|
||||
|
||||
Managed Cloud provisions only plugins selected from the current release image
|
||||
catalog. The database registration's plugin key, package name, version and
|
||||
catalog. A Cloud tenant token or managed configuration keeps this image-only
|
||||
policy active. Without an explicit selected package list, Cloud selects no
|
||||
packages and cannot fall back to local installs. An explicit operator-managed
|
||||
list also keeps this policy active outside Cloud. The database registration's plugin key, package name, version and
|
||||
approved manifest identify that package. Its legacy `packagePath` remains for
|
||||
local/self-hosted compatibility; Cloud never uses that path to locate code.
|
||||
Workers, plugin migrations and UI files resolve through the same current-image
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@paperclipai/plugin-daytona",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.9",
|
||||
"description": "Daytona sandbox provider plugin for Paperclip environments",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/paperclipai/paperclip",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@paperclipai/plugin-kubernetes",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"description": "Kubernetes sandbox provider plugin for Paperclip environments",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/paperclipai/paperclip",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { rmSync, writeFileSync, symlinkSync, mkdirSync } from "node:fs";
|
||||
import { rmSync, writeFileSync, symlinkSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { bundledImageFixture } from "./helpers/bundled-image-fixture.js";
|
||||
@@ -6,6 +6,17 @@ import { createBundledPluginPackageResolver } from "../services/bundled-plugin-p
|
||||
import { BUNDLED_PLUGIN_CATALOG, resolveBundledPluginInstalls } from "../services/bundled-plugins.js";
|
||||
import { readDistributionPluginCatalog } from "../services/distribution-plugin-catalog.js";
|
||||
|
||||
import boxd from "../../../packages/plugins/sandbox-providers/boxd/src/manifest.js";
|
||||
import createos from "../../../packages/plugins/sandbox-providers/createos/src/manifest.js";
|
||||
import cloudflare from "../../../packages/plugins/sandbox-providers/cloudflare/src/manifest.js";
|
||||
import daytona from "../../../packages/plugins/sandbox-providers/daytona/src/manifest.js";
|
||||
import e2b from "../../../packages/plugins/sandbox-providers/e2b/src/manifest.js";
|
||||
import exe_dev from "../../../packages/plugins/sandbox-providers/exe-dev/src/manifest.js";
|
||||
import kubernetes from "../../../packages/plugins/sandbox-providers/kubernetes/src/manifest.js";
|
||||
import modal from "../../../packages/plugins/sandbox-providers/modal/src/manifest.js";
|
||||
import novita from "../../../packages/plugins/sandbox-providers/novita/src/manifest.js";
|
||||
|
||||
const manifests = { boxd, createos, cloudflare, daytona, e2b, "exe-dev": exe_dev, kubernetes, modal, novita };
|
||||
const roots: string[] = [];
|
||||
function fixture() { const result = bundledImageFixture(); roots.push(result.root); return result; }
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
@@ -56,11 +67,20 @@ describe("bundled image package identity", () => {
|
||||
expect(() => resolver.resolve(registration)).toThrow(/escapes/);
|
||||
});
|
||||
|
||||
it("applies the same identity and containment checks to managed built-in packages", () => {
|
||||
const { root, packageRoot, manifest, registration } = fixture();
|
||||
const installs = resolveBundledPluginInstalls(["kubernetes"], { catalogRoot: root, enforceCatalogRoot: true, distributionPlugins: [], env: {} });
|
||||
installs[0] = { ...installs[0]!, localPath: packageRoot, pluginKey: manifest.id };
|
||||
expect(createBundledPluginPackageResolver(installs).resolve(registration)).toBe(packageRoot);
|
||||
expect(() => createBundledPluginPackageResolver(installs).resolve({ ...registration, version: "2.0.0" })).toThrow(/identity\/version/);
|
||||
it.each(BUNDLED_PLUGIN_CATALOG)("accepts the real shipped $key package and manifest contract", (entry) => {
|
||||
const manifest = manifests[entry.key as keyof typeof manifests];
|
||||
const pkg = JSON.parse(readFileSync(new URL(`../../../packages/plugins/${entry.relativePath}/package.json`, import.meta.url), "utf8"));
|
||||
const { root } = fixture();
|
||||
const packageRoot = path.join(root, entry.relativePath);
|
||||
mkdirSync(path.join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify(pkg));
|
||||
writeFileSync(path.join(packageRoot, "dist", "manifest.js"), `export default ${JSON.stringify(manifest)};`);
|
||||
writeFileSync(path.join(packageRoot, "dist", "worker.js"), "export default {};");
|
||||
const installs = resolveBundledPluginInstalls([entry.key], { catalogRoot: root, enforceCatalogRoot: true, distributionPlugins: [], env: {} });
|
||||
const resolver = createBundledPluginPackageResolver(installs);
|
||||
const registration = { pluginKey: entry.pluginKey, packageName: pkg.name, version: manifest.version, manifestJson: manifest };
|
||||
expect(pkg.version).toBe(manifest.version);
|
||||
expect(resolver.resolve(registration)).toBe(packageRoot);
|
||||
expect(resolver.inspect(registration, manifest)).toMatchObject({ compatible: true });
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { rmSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { rmSync, mkdirSync, writeFileSync, cpSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { applyPendingMigrations, closeRegisteredClients, companies, createDb, plugins } from "@paperclipai/db";
|
||||
@@ -14,6 +14,7 @@ import { pluginUiStaticRoutes } from "../routes/plugin-ui-static.js";
|
||||
import { errorHandler } from "../middleware/error-handler.js";
|
||||
import { derivePluginDatabaseNamespace } from "../services/plugin-database.js";
|
||||
import { ensureBundledPlugins } from "../services/bundled-plugins.js";
|
||||
import daytonaManifest from "../../../packages/plugins/sandbox-providers/daytona/src/manifest.js";
|
||||
import { bundledImageFixture } from "./helpers/bundled-image-fixture.js";
|
||||
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
|
||||
|
||||
@@ -193,6 +194,29 @@ describePostgres("bundled image registration in PostgreSQL", () => {
|
||||
await db.execute((await import("drizzle-orm")).sql.raw(`DROP SCHEMA "${derivePluginDatabaseNamespace(image.manifest.id)}" CASCADE`));
|
||||
});
|
||||
|
||||
it("retains registry-install provider credentials for an ordinary npm package", async () => {
|
||||
const npmManifest = { ...daytonaManifest, id: "paperclip.image-npm-fixture" };
|
||||
const image = bundledImageFixture(npmManifest);
|
||||
roots.push(image.root);
|
||||
const localPluginDir = path.join(image.root, "npm-home");
|
||||
const packageRoot = path.join(localPluginDir, "node_modules", "@paperclipai", "plugin-daytona");
|
||||
mkdirSync(path.dirname(packageRoot), { recursive: true });
|
||||
cpSync(image.packageRoot, packageRoot, { recursive: true });
|
||||
const metadata = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
||||
metadata.name = "@paperclipai/plugin-daytona";
|
||||
writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify(metadata));
|
||||
const registry = pluginRegistryService(db);
|
||||
const row = (await registry.install({ packageName: metadata.name }, npmManifest))!;
|
||||
await registry.updateStatus(row.id, { status: "ready" });
|
||||
vi.stubEnv("DAYTONA_API_KEY", "synthetic-provider-key");
|
||||
try {
|
||||
const current = runtime();
|
||||
const loader = pluginLoader(db, { localPluginDir, enableNpmDiscovery: false, enableLocalFilesystem: false }, current.services as unknown as PluginRuntimeServices);
|
||||
expect((await loader.loadSingle(row.id)).success).toBe(true);
|
||||
expect(current.services.workerManager.startWorker).toHaveBeenCalledWith(row.id, expect.objectContaining({ env: expect.objectContaining({ DAYTONA_API_KEY: "synthetic-provider-key" }) }));
|
||||
} finally { vi.unstubAllEnvs(); }
|
||||
});
|
||||
|
||||
it("keeps self-hosted local-path loading available without a managed resolver", async () => {
|
||||
const image = fixture();
|
||||
const loader = pluginLoader(db, { localPluginDir: path.join(image.root, "selfhost"), enableNpmDiscovery: false, enableLocalFilesystem: false });
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ensureBundledPlugins,
|
||||
resolveBundledCatalogRoot,
|
||||
resolveBundledPluginInstalls,
|
||||
resolveManagedBundledPluginKeys,
|
||||
type BundledPluginProvisionerDeps,
|
||||
type ResolvedBundledPlugin,
|
||||
} from "../services/bundled-plugins.js";
|
||||
@@ -30,6 +31,21 @@ afterAll(() => {
|
||||
// Catalog-root resolution (fail-to-start allowlist)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("managed bundled plugin selection", () => {
|
||||
it.each([
|
||||
{ env: {}, selected: undefined, expected: null },
|
||||
{ env: { PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN: "tenant-fixture" }, selected: undefined, expected: [] },
|
||||
{ env: { PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN: " " }, selected: undefined, expected: null },
|
||||
{ env: { PAPERCLIP_MANAGED_CONFIG: "" }, selected: undefined, expected: [] },
|
||||
{ env: { PAPERCLIP_MANAGED_CONFIG: "{}" }, selected: null, expected: [] },
|
||||
{ env: { PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN: "tenant-fixture" }, selected: ["daytona"], expected: ["daytona"] },
|
||||
{ env: {}, selected: ["modal"], expected: ["modal"] },
|
||||
{ env: {}, selected: [], expected: [] },
|
||||
])("uses the canonical Cloud floor and preserves explicit selection: $env / $selected", ({ env, selected, expected }) => {
|
||||
expect(resolveManagedBundledPluginKeys(selected, env)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveBundledPluginInstalls", () => {
|
||||
it("resolves the optional CreateOS sandbox provider without making it a self-hosted default", () => {
|
||||
expect(resolveBundledPluginInstalls(["createos"], {
|
||||
|
||||
+2
-1
@@ -183,6 +183,7 @@ import {
|
||||
ensureBundledPlugins,
|
||||
resolveBundledCatalogRoot,
|
||||
resolveBundledPluginInstalls,
|
||||
resolveManagedBundledPluginKeys,
|
||||
} from "./services/bundled-plugins.js";
|
||||
import { createBundledPluginPackageResolver } from "./services/bundled-plugin-package.js";
|
||||
import { readDistributionPluginCatalog, distributionPluginActivationGuard } from "./services/distribution-plugin-catalog.js";
|
||||
@@ -624,7 +625,7 @@ export async function createApp(
|
||||
res.set("Cache-Control", "no-store"); res.set("Referrer-Policy", "no-referrer");
|
||||
res.redirect(303, await chatChannels.githubWizard.identityCallback(String(req.query.state ?? ""), String(req.query.code ?? ""), req.actor));
|
||||
});
|
||||
const managedAutoInstallKeys = opts.managedPluginAutoInstall ?? null;
|
||||
const managedAutoInstallKeys = resolveManagedBundledPluginKeys(opts.managedPluginAutoInstall);
|
||||
const bundledCatalogRoot =
|
||||
opts.bundledPluginCatalogRoot ?? resolveBundledCatalogRoot(process.env);
|
||||
const distributionPlugins = readDistributionPluginCatalog(bundledCatalogRoot, BUNDLED_PLUGIN_CATALOG);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import { isCloudManagedInstance } from "./cloud-instance.js";
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/shared";
|
||||
import { assertDistributionManifestCapabilities, readDistributionPluginCatalog, type DistributionPlugin } from "./distribution-plugin-catalog.js";
|
||||
|
||||
@@ -118,6 +119,13 @@ export const BUNDLED_PLUGIN_CATALOG: readonly BundledPluginCatalogEntry[] = [
|
||||
*/
|
||||
export const SELF_HOSTED_AUTO_INSTALL_KEYS: readonly string[] = ["kubernetes"];
|
||||
|
||||
export function resolveManagedBundledPluginKeys(
|
||||
keys: readonly string[] | null | undefined,
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): readonly string[] | null {
|
||||
return keys ?? (isCloudManagedInstance(env) ? [] : null);
|
||||
}
|
||||
|
||||
export function resolveBundledCatalogRoot(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
|
||||
@@ -2338,7 +2338,7 @@ export function pluginLoader(
|
||||
env: buildPluginWorkerEnv({
|
||||
manifest,
|
||||
packageName: activePlugin.packageName,
|
||||
packagePath: packageRoot,
|
||||
packagePath: resolvePackageRoot ? packageRoot : activePlugin.packagePath,
|
||||
instanceInfo,
|
||||
}),
|
||||
// Authorize the worker to act on each configured company from its
|
||||
|
||||
Reference in new issue
Block a user