diff --git a/server/src/__tests__/adapter-routes.test.ts b/server/src/__tests__/adapter-routes.test.ts index b90869e6..ccc0edcb 100644 --- a/server/src/__tests__/adapter-routes.test.ts +++ b/server/src/__tests__/adapter-routes.test.ts @@ -46,6 +46,7 @@ const overridingConfigSchemaAdapter: ServerAdapterModule = { let registerServerAdapter: typeof import("../adapters/registry.js").registerServerAdapter; let unregisterServerAdapter: typeof import("../adapters/registry.js").unregisterServerAdapter; let findServerAdapter: typeof import("../adapters/registry.js").findServerAdapter; +let findActiveServerAdapter: typeof import("../adapters/registry.js").findActiveServerAdapter; let setOverridePaused: typeof import("../adapters/registry.js").setOverridePaused; let adapterRoutes: typeof import("../routes/adapters.js").adapterRoutes; let errorHandler: typeof import("../middleware/index.js").errorHandler; @@ -109,6 +110,7 @@ describe("adapter routes", () => { registerServerAdapter = registry.registerServerAdapter; unregisterServerAdapter = registry.unregisterServerAdapter; findServerAdapter = registry.findServerAdapter; + findActiveServerAdapter = registry.findActiveServerAdapter; setOverridePaused = registry.setOverridePaused; adapterRoutes = routes.adapterRoutes; errorHandler = middleware.errorHandler; @@ -339,4 +341,54 @@ describe("adapter routes", () => { unregisterServerAdapter(HOT_INSTALL_TYPE); }); + + it("POST /api/adapters/install allows an external adapter to override a builtin type", async () => { + const builtin = findServerAdapter("codex_local"); + expect(builtin).not.toBeNull(); + + const externalModule: ServerAdapterModule = { + type: "codex_local", + execute: async () => ({ exitCode: 0, signal: null, timedOut: false }), + testEnvironment: async () => ({ + adapterType: "codex_local", + status: "pass", + checks: [], + testedAt: new Date(0).toISOString(), + }), + models: [{ id: "plugin-codex", label: "Plugin Codex" }], + }; + mockPluginLoader.loadExternalAdapterPackage.mockResolvedValue(externalModule); + + const app = createApp({ isInstanceAdmin: true }); + const res = await request(app) + .post("/api/adapters/install") + .send({ packageName: "/tmp/fake-codex-override", isLocalPath: true }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(res.body.type).toBe("codex_local"); + const registeredOverride = findServerAdapter("codex_local"); + expect(registeredOverride).toMatchObject({ + type: "codex_local", + models: [{ id: "plugin-codex", label: "Plugin Codex" }], + }); + + setOverridePaused("codex_local", true); + expect(findActiveServerAdapter("codex_local")).toBe(builtin); + + mockAdapterPluginStore.getAdapterPluginByType.mockReturnValue({ + type: "codex_local", + packageName: undefined, + localPath: "/tmp/fake-codex-override", + installedAt: new Date(0).toISOString(), + }); + mockAdapterPluginStore.removeAdapterPlugin.mockReturnValue(true); + + const removed = await request(app).delete("/api/adapters/codex_local"); + expect(removed.status, JSON.stringify(removed.body)).toBe(200); + expect(removed.body).toMatchObject({ type: "codex_local", removed: true }); + + unregisterServerAdapter("codex_local"); + expect(findServerAdapter("codex_local")).toBe(builtin); + setOverridePaused("codex_local", false); + }); }); diff --git a/server/src/routes/adapters.ts b/server/src/routes/adapters.ts index afdc6762..55d766f4 100644 --- a/server/src/routes/adapters.ts +++ b/server/src/routes/adapters.ts @@ -296,17 +296,16 @@ export function adapterRoutes() { // Load and register the adapter (use canonicalName for path resolution) const adapterModule = await loadExternalAdapterPackage(canonicalName, moduleLocalPath); - // Check if this type conflicts with a built-in adapter - if (BUILTIN_ADAPTER_TYPES.has(adapterModule.type)) { - res.status(409).json({ - error: `Adapter type "${adapterModule.type}" is a built-in adapter and cannot be overwritten.`, - }); - return; - } + // External adapters may intentionally override built-in adapter types. + // registerServerAdapter preserves the built-in as a fallback so pausing or + // removing the override restores the original implementation. - // Check if already registered (indicates a reinstall/update) + // Check if already registered (indicates a reinstall/update). + // For built-in types the registry always returns the built-in, so we + // additionally require an existing external plugin record to + // distinguish a true reinstall from a first-time override. const existing = findServerAdapter(adapterModule.type); - const isReinstall = existing !== null; + const isReinstall = existing !== null && !!getAdapterPluginByType(adapterModule.type); if (existing) { unregisterServerAdapter(adapterModule.type); logger.info({ type: adapterModule.type }, "Unregistered existing adapter for replacement"); @@ -446,8 +445,9 @@ export function adapterRoutes() { return; } - // Prevent removal of built-in adapters - if (BUILTIN_ADAPTER_TYPES.has(adapterType)) { + // Prevent removal of built-in adapters, unless this built-in type is + // currently backed by an external adapter plugin override. + if (BUILTIN_ADAPTER_TYPES.has(adapterType) && !getAdapterPluginByType(adapterType)) { res.status(403).json({ error: `Cannot remove built-in adapter "${adapterType}".`, });