diff --git a/server/src/__tests__/plugin-routes-authz.test.ts b/server/src/__tests__/plugin-routes-authz.test.ts
index 9447bc93928..90371991576 100644
--- a/server/src/__tests__/plugin-routes-authz.test.ts
+++ b/server/src/__tests__/plugin-routes-authz.test.ts
@@ -146,11 +146,18 @@ describe.sequential("plugin install and upgrade authz", () => {
expect(res.status).toBe(200);
const packageNames = res.body.map((plugin: { packageName: string }) => plugin.packageName);
+ const byPackageName = new Map(
+ res.body.map((plugin: { packageName: string; experimental: boolean }) => [plugin.packageName, plugin]),
+ );
expect(packageNames).toContain("@paperclipai/plugin-workspace-diff");
expect(packageNames).toContain("@paperclipai/plugin-llm-wiki");
expect(packageNames).toContain("@paperclipai/plugin-modal");
expect(packageNames).toContain("@paperclipai/plugin-authoring-smoke-example");
expect(packageNames).not.toContain("@paperclipai/plugin-sdk");
+ expect(byPackageName.get("@paperclipai/plugin-workspace-diff")?.experimental).toBe(true);
+ expect(byPackageName.get("@paperclipai/plugin-llm-wiki")?.experimental).toBe(true);
+ expect(byPackageName.get("@paperclipai/plugin-modal")?.experimental).toBe(true);
+ expect(byPackageName.get("@paperclipai/plugin-authoring-smoke-example")?.experimental).toBe(false);
}, 20_000);
it("rejects plugin installation for non-admin board users", async () => {
diff --git a/server/src/routes/plugins.ts b/server/src/routes/plugins.ts
index 9f48a0dc5f5..16434ef9011 100644
--- a/server/src/routes/plugins.ts
+++ b/server/src/routes/plugins.ts
@@ -121,6 +121,7 @@ interface AvailableBundledPlugin {
description: string;
localPath: string;
tag: "example" | "first-party";
+ experimental: boolean;
}
/** Response body for GET /api/plugins/:pluginId/health */
@@ -150,6 +151,10 @@ const PLUGIN_SCOPED_API_RESPONSE_HEADER_ALLOWLIST = new Set([
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "../../..");
+const EXPERIMENTAL_BUNDLED_PLUGIN_PACKAGE_NAMES = new Set([
+ "@paperclipai/plugin-llm-wiki",
+ "@paperclipai/plugin-workspace-diff",
+]);
function titleCasePluginName(packageName: string): string {
const localName = packageName.split("/").pop() ?? packageName;
@@ -244,6 +249,14 @@ function bundledPluginMetadata(
}
}
+function isExperimentalBundledPlugin(packageRoot: string, packageName: string): boolean {
+ return (
+ EXPERIMENTAL_BUNDLED_PLUGIN_PACKAGE_NAMES.has(packageName)
+ || packageRoot.includes(`${path.sep}sandbox-providers${path.sep}`)
+ || packageName.includes("sandbox")
+ );
+}
+
function listBundledPlugins(): AvailableBundledPlugin[] {
const pluginRoot = path.resolve(REPO_ROOT, "packages/plugins");
return findPackageJsonFiles(pluginRoot)
@@ -274,6 +287,7 @@ function listBundledPlugins(): AvailableBundledPlugin[] {
?? `Bundled Paperclip plugin from ${path.relative(REPO_ROOT, packageRoot)}.`,
localPath: packageRoot,
tag,
+ experimental: isExperimentalBundledPlugin(packageRoot, packageName),
},
];
})
diff --git a/ui/src/api/plugins.ts b/ui/src/api/plugins.ts
index 8f4a548612d..b018756091e 100644
--- a/ui/src/api/plugins.ts
+++ b/ui/src/api/plugins.ts
@@ -139,6 +139,7 @@ export interface AvailableBundledPlugin {
description: string;
localPath: string;
tag: "example" | "first-party";
+ experimental: boolean;
}
export interface PluginLocalFolderProblem {
diff --git a/ui/src/pages/PluginManager.tsx b/ui/src/pages/PluginManager.tsx
index d03c1f95771..7ae4f2dc301 100644
--- a/ui/src/pages/PluginManager.tsx
+++ b/ui/src/pages/PluginManager.tsx
@@ -43,6 +43,17 @@ function getPluginErrorSummary(plugin: PluginRecord): string {
return firstNonEmptyLine(plugin.lastError) ?? "Plugin entered an error state without a stored error message.";
}
+function ExperimentalBadge() {
+ return (
+
+ Experimental
+
+ );
+}
+
/**
* PluginManager page component.
*
@@ -249,6 +260,7 @@ export function PluginManager() {
{bundledPlugin.tag === "first-party" ? "First-party" : "Example"}
+ {bundledPlugin.experimental && }
{installedPlugin ? (
)}
+ {bundledByPackageName.get(plugin.packageName)?.experimental && }