mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
fix(adapters): restrict Cloud to release image implementations
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d7f63370e5
commit
e6346dbfe6
10 files changed
+219
-49
No files matched your search
+3
-1
@@ -1488,7 +1488,9 @@ Paperclip also persists an empty `OPENAI_API_KEY` override for those agents so a
|
||||
|
||||
If the `codex` CLI is not installed or not on `PATH`, `codex_local` agent runs fail at execution time with a clear adapter error. Quota polling uses a short-lived `codex app-server` subprocess: when `codex` cannot be spawned, that provider reports `ok: false` in aggregated quota results and the API server keeps running (it must not exit on a missing binary).
|
||||
|
||||
Local adapters require their corresponding CLI/session setup on the machine running Paperclip. External adapters are installed through the adapter/plugin flow and should not require hardcoded imports in `server/` or `ui/`.
|
||||
Local adapters require their corresponding CLI/session setup on the machine running Paperclip. Self-hosted instances can install external adapters through the adapter/plugin flow without hardcoded imports in `server/` or `ui/`.
|
||||
|
||||
Cloud-managed instances use only adapter implementations supplied by the release image. A tenant server token or the presence of `PAPERCLIP_MANAGED_CONFIG` enforces this boundary. Startup and UI parser requests do not read external adapter registrations or packages. Install, reinstall, reload, remove, and override operations return `403` with `adapter_install_platform_managed`, including for instance admins. Legacy `adapter-plugins.json` files stay untouched and do not change the built-in registry.
|
||||
|
||||
### Filesystem access boundary (2026-10-20)
|
||||
|
||||
|
||||
@@ -55,7 +55,14 @@ Policy: Company model discovery requires company access; account-backed discover
|
||||
also enforces its account/pool and target-agent rules. Instance adapter inventory
|
||||
and installation retain board/admin gates. Delegated managed
|
||||
AI-account login operations are enumerated under `model-accounts`; they do not
|
||||
grant raw host administration or execution-session reset.
|
||||
grant raw host administration or execution-session reset. On Cloud-managed
|
||||
instances, external adapter install, reinstall, reload, removal and override
|
||||
operations return `403` with `adapter_install_platform_managed`, including for
|
||||
instance admins. Built-in metadata remains available to board users. Startup
|
||||
and UI parser reads do not consume saved external registrations or package code.
|
||||
[Local fixture controls](../../server/src/adapters/cloud-external-adapters.test.ts)
|
||||
and [route controls](../../server/src/__tests__/adapter-routes-authz.test.ts)
|
||||
cover this boundary; they do not establish live agent or provider qualification.
|
||||
|
||||
Evidence: `source-only` — [implementation](../../server/src/routes/agents.ts#L6608) at
|
||||
the reviewed commit above.
|
||||
|
||||
@@ -170,6 +170,15 @@ controls do not establish live provider acceptance.
|
||||
|
||||
### `adapter-setup`
|
||||
|
||||
Cloud-managed instances load adapters from the release image. Saved external
|
||||
registrations cannot replace built-ins or supply UI parser code. Install,
|
||||
reinstall, reload, remove, and override operations remain platform-managed.
|
||||
The [fixture loader controls](../../server/src/adapters/cloud-external-adapters.test.ts)
|
||||
exercise real packages, import side effects, both managed-instance signals and
|
||||
self-hosted compatibility. The [route authorization controls](../../server/src/__tests__/adapter-routes-authz.test.ts)
|
||||
check managed denials before package or registration access. These are local
|
||||
controls, not live Cloud provider qualification.
|
||||
|
||||
Automated: [adapter validation routes](../../server/src/__tests__/agent-adapter-validation-routes.test.ts) and [model refresh](../../server/src/__tests__/adapter-model-refresh-routes.test.ts) cover server behavior.
|
||||
|
||||
Claude native setup uses the existing Anthropic API-key or subscription connection
|
||||
|
||||
@@ -373,35 +373,31 @@ describe("adapter management route authorization", () => {
|
||||
},
|
||||
);
|
||||
|
||||
describe("cloud-managed adapter code install floor", () => {
|
||||
beforeEach(() => {
|
||||
process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN = "test-server-token";
|
||||
describe("cloud-managed adapter code floor", () => {
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
it.each(
|
||||
["PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN", "PAPERCLIP_MANAGED_CONFIG"].flatMap((signal) =>
|
||||
["install", "reinstall", "reload", "override", "delete"].map((routeName) => [signal, routeName]),
|
||||
),
|
||||
)("blocks %s adapter %s before package or registration access", async (signal, routeName) => {
|
||||
seedInstalledExternalAdapter();
|
||||
vi.clearAllMocks();
|
||||
vi.stubEnv(signal, signal === "PAPERCLIP_MANAGED_CONFIG" ? "" : "test-server-token");
|
||||
const app = createApp(instanceAdmin);
|
||||
|
||||
const res = await sendMutatingRequest(app, routeName);
|
||||
|
||||
expect(res.status, `${routeName}: ${JSON.stringify(res.body)}`).toBe(403);
|
||||
expect(res.body.details).toMatchObject({ code: "adapter_install_platform_managed" });
|
||||
expect(mocks.execFile).not.toHaveBeenCalled();
|
||||
expect(mocks.loadExternalAdapterPackage).not.toHaveBeenCalled();
|
||||
expect(mocks.reloadExternalAdapter).not.toHaveBeenCalled();
|
||||
expect(mocks.getAdapterPluginByType).not.toHaveBeenCalled();
|
||||
expect(mocks.addAdapterPlugin).not.toHaveBeenCalled();
|
||||
expect(mocks.removeAdapterPlugin).not.toHaveBeenCalled();
|
||||
expect(mocks.externalRecords.get(EXTERNAL_ADAPTER_TYPE)).toEqual(installedRecord());
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN;
|
||||
});
|
||||
|
||||
it.each(["install", "reinstall"] as const)(
|
||||
"floors adapter %s off for instance admins on cloud-managed instances",
|
||||
async (routeName) => {
|
||||
resetInstalledExternalAdapterState();
|
||||
if (routeName !== "install") {
|
||||
seedInstalledExternalAdapter();
|
||||
}
|
||||
const app = createApp(instanceAdmin);
|
||||
|
||||
const res = await sendMutatingRequest(app, routeName);
|
||||
|
||||
expect(res.status, `${routeName}: ${JSON.stringify(res.body)}`).toBe(
|
||||
403,
|
||||
);
|
||||
expect(res.body.details).toMatchObject({
|
||||
code: "adapter_install_platform_managed",
|
||||
});
|
||||
expect(mocks.execFile).not.toHaveBeenCalled();
|
||||
expect(mocks.loadExternalAdapterPackage).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("operator-hidden adapter management floor", () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -49,7 +49,7 @@ import type {
|
||||
import { loadExternalAdapterPackage, getUiParserSource, getOrExtractUiParserSource, reloadExternalAdapter } from "../adapters/plugin-loader.js";
|
||||
import { logger } from "../middleware/logger.js";
|
||||
import { badRequest, forbidden } from "../errors.js";
|
||||
import { isCloudManagedInstance } from "../services/cloud-instance.js";
|
||||
import { assertExternalAdapterCodeAllowed } from "../services/adapter-code-policy.js";
|
||||
import { getHiddenSettings } from "../services/settings-visibility.js";
|
||||
import { assertBoardOrgAccess, assertCompanyAccess, assertInstanceAdmin, getActorInfo } from "./authz.js";
|
||||
import { BUILTIN_ADAPTER_TYPES } from "../adapters/builtin-adapter-types.js";
|
||||
@@ -60,22 +60,6 @@ import { resolveAntigravityCandidateAdmission } from "../services/native-runtime
|
||||
const execFileAsync = promisify(execFile);
|
||||
let warnedMalformedAntigravityAdmission = false;
|
||||
|
||||
/**
|
||||
* Floor: on cloud-managed instances adapter code is bundled into the platform
|
||||
* image; fetching and loading external adapter packages at runtime stays off
|
||||
* for every actor, including instance admins. Adapter code executes in the
|
||||
* server process, so a runtime install would let an instance admin read the
|
||||
* platform trust anchors from the process environment (mirrors the
|
||||
* bundled-only plugin install floor in plugin-install-guard.ts).
|
||||
*/
|
||||
function assertAdapterCodeInstallAllowed() {
|
||||
if (isCloudManagedInstance()) {
|
||||
throw forbidden("Adapter installation is platform-managed on cloud-managed instances", {
|
||||
code: "adapter_install_platform_managed",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Floor: when the hosting operator hides the Adapters settings surface
|
||||
* (`instance.adapters` in PAPERCLIP_HIDDEN_SETTINGS), adapter management
|
||||
@@ -345,7 +329,7 @@ export function adapterRoutes(options: {
|
||||
*/
|
||||
router.post("/adapters/install", async (req, res) => {
|
||||
assertInstanceAdmin(req);
|
||||
assertAdapterCodeInstallAllowed();
|
||||
assertExternalAdapterCodeAllowed();
|
||||
assertAdapterManagementVisible();
|
||||
|
||||
const { packageName, isLocalPath = false, version } = req.body as AdapterInstallRequest;
|
||||
@@ -551,6 +535,7 @@ export function adapterRoutes(options: {
|
||||
*/
|
||||
router.patch("/adapters/:type/override", async (req, res) => {
|
||||
assertInstanceAdmin(req);
|
||||
assertExternalAdapterCodeAllowed();
|
||||
|
||||
assertAdapterManagementVisible();
|
||||
|
||||
@@ -581,6 +566,7 @@ export function adapterRoutes(options: {
|
||||
*/
|
||||
router.delete("/adapters/:type", async (req, res) => {
|
||||
assertInstanceAdmin(req);
|
||||
assertExternalAdapterCodeAllowed();
|
||||
assertAdapterManagementVisible();
|
||||
|
||||
const adapterType = req.params.type;
|
||||
@@ -658,6 +644,7 @@ export function adapterRoutes(options: {
|
||||
*/
|
||||
router.post("/adapters/:type/reload", async (req, res) => {
|
||||
assertInstanceAdmin(req);
|
||||
assertExternalAdapterCodeAllowed();
|
||||
assertAdapterManagementVisible();
|
||||
|
||||
const type = req.params.type;
|
||||
@@ -711,7 +698,7 @@ export function adapterRoutes(options: {
|
||||
// package name, but without the risk of losing the store record.
|
||||
router.post("/adapters/:type/reinstall", async (req, res) => {
|
||||
assertInstanceAdmin(req);
|
||||
assertAdapterCodeInstallAllowed();
|
||||
assertExternalAdapterCodeAllowed();
|
||||
assertAdapterManagementVisible();
|
||||
|
||||
const type = req.params.type;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { forbidden } from "../errors.js";
|
||||
import { isCloudManagedInstance } from "./cloud-instance.js";
|
||||
|
||||
export function assertExternalAdapterCodeAllowed(): void {
|
||||
if (isCloudManagedInstance()) {
|
||||
throw forbidden("Adapter installation is platform-managed on cloud-managed instances", {
|
||||
code: "adapter_install_platform_managed",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolvePaperclipHomeDir } from "../home-paths.js";
|
||||
import { isCloudManagedInstance } from "./cloud-instance.js";
|
||||
import { assertExternalAdapterCodeAllowed } from "./adapter-code-policy.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -79,6 +81,7 @@ function ensureDirs(): string {
|
||||
}
|
||||
|
||||
function readStore(): AdapterPluginRecord[] {
|
||||
if (isCloudManagedInstance()) return [];
|
||||
const { adapterPluginsStorePath } = adapterPluginPaths();
|
||||
if (storeCache?.path === adapterPluginsStorePath) return storeCache.records;
|
||||
try {
|
||||
@@ -130,11 +133,16 @@ function writeSettings(settings: AdapterSettings): void {
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function getAdapterPluginStorePath(): string {
|
||||
return adapterPluginPaths().adapterPluginsStorePath;
|
||||
}
|
||||
|
||||
export function listAdapterPlugins(): AdapterPluginRecord[] {
|
||||
return readStore();
|
||||
}
|
||||
|
||||
export function addAdapterPlugin(record: AdapterPluginRecord): void {
|
||||
assertExternalAdapterCodeAllowed();
|
||||
const store = [...readStore()];
|
||||
const idx = store.findIndex((r) => r.type === record.type);
|
||||
if (idx >= 0) {
|
||||
@@ -146,6 +154,7 @@ export function addAdapterPlugin(record: AdapterPluginRecord): void {
|
||||
}
|
||||
|
||||
export function removeAdapterPlugin(type: string): boolean {
|
||||
assertExternalAdapterCodeAllowed();
|
||||
const store = [...readStore()];
|
||||
const idx = store.findIndex((r) => r.type === type);
|
||||
if (idx < 0) return false;
|
||||
@@ -159,6 +168,7 @@ export function getAdapterPluginByType(type: string): AdapterPluginRecord | unde
|
||||
}
|
||||
|
||||
export function getAdapterPluginsDir(): string {
|
||||
assertExternalAdapterCodeAllowed();
|
||||
return ensureDirs();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user