fix(adapters): restrict Cloud to release image implementations

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-30 11:17:58 -05:00
1 parent d7f63370e5
commit e6346dbfe6
10 files changed
+219 -49

No files matched your search

@@ -0,0 +1,136 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let home: string;
let localPackage: string;
let npmPackage: string;
let storePath: string;
let sentinel: string;
const parserSource = "export function parseStdoutLine() { return { message: 'external parser' }; }";
const managedError = { status: 403, details: { code: "adapter_install_platform_managed" } };
function writePackage(packageDir: string, type: string): void {
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({
name: `fixture-${type}`,
type: "module",
exports: { ".": "./index.mjs", "./ui-parser": "./ui-parser.mjs" },
paperclip: { adapterUiParser: "1.0.0" },
}));
fs.writeFileSync(path.join(packageDir, "index.mjs"), `
import fs from "node:fs";
fs.appendFileSync(${JSON.stringify(sentinel)}, ${JSON.stringify(type + "\n")});
export function createServerAdapter() {
return {
type: ${JSON.stringify(type)},
label: "External fixture",
models: [],
execute: async () => ({ exitCode: 41, signal: null, timedOut: false }),
testEnvironment: async () => ({ status: "pass", checks: [] }),
};
}
`);
fs.writeFileSync(path.join(packageDir, "ui-parser.mjs"), parserSource);
}
beforeEach(() => {
vi.resetModules();
vi.stubEnv("PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN", undefined);
vi.stubEnv("PAPERCLIP_MANAGED_CONFIG", undefined);
home = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-cloud-adapters-"));
vi.stubEnv("PAPERCLIP_HOME", home);
sentinel = path.join(home, "external-code-executed");
localPackage = path.join(home, "local-package");
npmPackage = path.join(home, "adapter-plugins", "node_modules", "fixture-npm");
storePath = path.join(home, "adapter-plugins.json");
writePackage(localPackage, "codex_local");
writePackage(npmPackage, "fixture_npm");
fs.writeFileSync(storePath, JSON.stringify([
{ packageName: "fixture-local", localPath: localPackage, type: "codex_local", installedAt: "2026-01-01" },
{ packageName: "fixture-npm", type: "fixture_npm", installedAt: "2026-01-01" },
]));
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
fs.rmSync(home, { recursive: true, force: true });
});
describe("Cloud image adapter boundary", () => {
it.each(["PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN", "PAPERCLIP_MANAGED_CONFIG"])(
"cold boots built-ins without reading or executing persisted packages with %s",
async (signal) => {
vi.stubEnv(signal, signal === "PAPERCLIP_MANAGED_CONFIG" ? "" : "test-token");
const registrationBefore = fs.readFileSync(storePath, "utf8");
const reads = vi.spyOn(fs, "readFileSync");
const registry = await import("./registry.js");
await registry.waitForExternalAdapters();
const loader = await import("./plugin-loader.js");
const store = await import("../services/adapter-plugin-store.js");
expect(registry.requireServerAdapter("codex_local").label).not.toBe("External fixture");
expect(registry.requireServerAdapter("process").execute).toBeTypeOf("function");
expect(registry.findServerAdapter("fixture_npm")).toBeNull();
expect(registry.hasActiveAdapterOverride("codex_local")).toBe(false);
expect(await loader.buildExternalAdapters()).toEqual([]);
expect(store.listAdapterPlugins()).toEqual([]);
expect(store.getAdapterPluginByType("codex_local")).toBeUndefined();
expect(loader.getUiParserSource("codex_local")).toBeUndefined();
expect(loader.getOrExtractUiParserSource("codex_local")).toBeUndefined();
await expect(loader.loadExternalAdapterPackage("fixture-local", localPackage)).rejects.toMatchObject(managedError);
await expect(loader.loadExternalAdapterPackage("fixture-npm")).rejects.toMatchObject(managedError);
await expect(loader.reloadExternalAdapter("codex_local")).rejects.toMatchObject(managedError);
expect(() => registry.registerServerAdapter(registry.requireServerAdapter("codex_local"))).toThrow();
expect(() => registry.unregisterServerAdapter("codex_local")).toThrow();
expect(() => registry.setOverridePaused("codex_local", false)).toThrow();
expect(() => store.addAdapterPlugin({ packageName: "fixture", type: "fixture", installedAt: "2026-01-01" })).toThrow();
expect(() => store.removeAdapterPlugin("codex_local")).toThrow();
expect(() => store.getAdapterPluginsDir()).toThrow();
expect(store.getAdapterPluginStorePath()).toBe(storePath);
expect(reads.mock.calls.map(([file]) => String(file)).filter(file =>
file === storePath || file.startsWith(localPackage + path.sep) || file.startsWith(npmPackage + path.sep),
)).toEqual([]);
reads.mockRestore();
expect(fs.existsSync(sentinel)).toBe(false);
expect(fs.readFileSync(storePath, "utf8")).toBe(registrationBefore);
},
);
it("preserves local and npm package loading, overrides, reload, and lazy UI parsing on self-hosted instances", async () => {
const loader = await import("./plugin-loader.js");
expect(loader.getOrExtractUiParserSource("codex_local")).toBe(parserSource);
const registry = await import("./registry.js");
await registry.waitForExternalAdapters();
const override = registry.requireServerAdapter("codex_local");
expect(override.label).toBe("External fixture");
expect(registry.hasActiveAdapterOverride("codex_local")).toBe(true);
expect(registry.requireServerAdapter("fixture_npm").label).toBe("External fixture");
expect(await override.execute({} as never)).toMatchObject({ exitCode: 41 });
expect(fs.readFileSync(sentinel, "utf8").trim().split("\n")).toEqual(["codex_local", "fixture_npm"]);
expect(loader.getUiParserSource("fixture_npm")).toBe(parserSource);
const reloaded = await loader.reloadExternalAdapter("codex_local");
expect(reloaded?.type).toBe("codex_local");
expect(await reloaded?.execute({} as never)).toMatchObject({ exitCode: 41 });
expect(registry.setOverridePaused("codex_local", true)).toBe(true);
expect(registry.findActiveServerAdapter("codex_local")?.label).not.toBe("External fixture");
expect(registry.setOverridePaused("codex_local", false)).toBe(true);
registry.unregisterServerAdapter("codex_local");
expect(registry.requireServerAdapter("codex_local").label).not.toBe("External fixture");
});
it("does not expose a cached registration or parser after a managed signal becomes present", async () => {
const loader = await import("./plugin-loader.js");
const store = await import("../services/adapter-plugin-store.js");
await loader.loadExternalAdapterPackage("fixture-local", localPackage);
expect(loader.getUiParserSource("codex_local")).toBe(parserSource);
expect(store.listAdapterPlugins()).toHaveLength(2);
vi.stubEnv("PAPERCLIP_MANAGED_CONFIG", "{}");
expect(store.listAdapterPlugins()).toEqual([]);
expect(loader.getUiParserSource("codex_local")).toBeUndefined();
expect(loader.getOrExtractUiParserSource("codex_local")).toBeUndefined();
});
});
+7
View File
@@ -15,6 +15,8 @@ import { pathToFileURL } from "node:url";
import type { ServerAdapterModule } from "./types.js";
import { validateAdapterLoginCapability } from "@paperclipai/adapter-utils";
import { logger } from "../middleware/logger.js";
import { isCloudManagedInstance } from "../services/cloud-instance.js";
import { assertExternalAdapterCodeAllowed } from "../services/adapter-code-policy.js";
import {
listAdapterPlugins,
@@ -30,6 +32,7 @@ import type { AdapterPluginRecord } from "../services/adapter-plugin-store.js";
const uiParserCache = new Map<string, string>();
export function getUiParserSource(adapterType: string): string | undefined {
if (isCloudManagedInstance()) return undefined;
return uiParserCache.get(adapterType);
}
@@ -38,6 +41,7 @@ export function getUiParserSource(adapterType: string): string | undefined {
* Makes the ui-parser.js endpoint self-healing.
*/
export function getOrExtractUiParserSource(adapterType: string): string | undefined {
if (isCloudManagedInstance()) return undefined;
const cached = uiParserCache.get(adapterType);
if (cached) return cached;
@@ -182,6 +186,7 @@ export async function loadExternalAdapterPackage(
packageName: string,
localPath?: string,
): Promise<ServerAdapterModule> {
assertExternalAdapterCodeAllowed();
const packageDir = localPath
? path.resolve(localPath)
: path.resolve(getAdapterPluginsDir(), "node_modules", packageName);
@@ -221,6 +226,7 @@ async function loadFromRecord(record: AdapterPluginRecord): Promise<ServerAdapte
export async function reloadExternalAdapter(
type: string,
): Promise<ServerAdapterModule | null> {
assertExternalAdapterCodeAllowed();
const record = getAdapterPluginByType(type);
if (!record) return null;
@@ -271,6 +277,7 @@ export async function reloadExternalAdapter(
* Build all external adapter modules from the plugin store.
*/
export async function buildExternalAdapters(): Promise<ServerAdapterModule[]> {
if (isCloudManagedInstance()) return [];
const results: ServerAdapterModule[] = [];
const storeRecords = listAdapterPlugins();
+6
View File
@@ -136,6 +136,8 @@ import {
import { agentConfigurationDoc as piAgentConfigurationDoc } from "@paperclipai/adapter-pi-local";
import { BUILTIN_ADAPTER_TYPES } from "./builtin-adapter-types.js";
import { buildExternalAdapters } from "./plugin-loader.js";
import { isCloudManagedInstance } from "../services/cloud-instance.js";
import { assertExternalAdapterCodeAllowed } from "../services/adapter-code-policy.js";
import { NativeEnvironmentProbeCleanupError } from "../vendor/paperclip-runner/index.js";
import { processAdapter } from "./process/index.js";
import { httpAdapter } from "./http/index.js";
@@ -1028,6 +1030,7 @@ export function resolveExternalAdapterRegistration(
* and avoid racing against the loading window.
*/
const externalAdaptersReady: Promise<void> = (async () => {
if (isCloudManagedInstance()) return;
try {
const externalAdapters = await buildExternalAdapters();
for (const externalAdapter of externalAdapters) {
@@ -1063,6 +1066,7 @@ export function waitForExternalAdapters(): Promise<void> {
}
export function registerServerAdapter(adapter: ServerAdapterModule): void {
assertExternalAdapterCodeAllowed();
if (BUILTIN_ADAPTER_TYPES.has(adapter.type) && !builtinFallbacks.has(adapter.type)) {
const existing = adaptersByType.get(adapter.type);
if (existing) {
@@ -1073,6 +1077,7 @@ export function registerServerAdapter(adapter: ServerAdapterModule): void {
}
export function unregisterServerAdapter(type: string): void {
assertExternalAdapterCodeAllowed();
if (type === processAdapter.type || type === httpAdapter.type) return;
if (builtinFallbacks.has(type)) {
pausedOverrides.delete(type);
@@ -1220,6 +1225,7 @@ export async function detectAdapterModel(
* an override or was already in the requested state.
*/
export function setOverridePaused(type: string, paused: boolean): boolean {
assertExternalAdapterCodeAllowed();
if (!builtinFallbacks.has(type)) return false;
const wasPaused = pausedOverrides.has(type);
if (paused && !wasPaused) {