From 0509d08f3c1ff3bc138fe98e3cb172f5afc782ea Mon Sep 17 00:00:00 2001 From: Paperclip Bot Date: Wed, 3 Jun 2026 14:08:15 +0000 Subject: [PATCH] Fix plugin company-scoped config resolution --- packages/plugins/sdk/src/types.ts | 1 + packages/plugins/sdk/src/worker-rpc-host.ts | 50 ++- .../plugins/sdk/tests/worker-rpc-host.test.ts | 313 +++++++++++++++--- ...lugin-host-services-session-events.test.ts | 129 ++++++++ .../src/__tests__/plugin-routes-authz.test.ts | 10 +- server/src/routes/plugins.ts | 46 ++- server/src/services/plugin-host-services.ts | 3 + server/src/services/plugin-registry.ts | 37 ++- 8 files changed, 505 insertions(+), 84 deletions(-) create mode 100644 server/src/__tests__/plugin-host-services-session-events.test.ts diff --git a/packages/plugins/sdk/src/types.ts b/packages/plugins/sdk/src/types.ts index 2ad6361e..9c436ab0 100644 --- a/packages/plugins/sdk/src/types.ts +++ b/packages/plugins/sdk/src/types.ts @@ -1510,6 +1510,7 @@ export interface AgentSession { export interface AgentSessionEvent { sessionId: string; runId: string; + companyId?: string | null; seq: number; /** The kind of event: "chunk" for output data, "status" for run state changes, "done" for end-of-stream, "error" for failures. */ eventType: "chunk" | "status" | "done" | "error"; diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index 727441e3..792ad367 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -168,6 +168,16 @@ interface RuntimeCompanyContext { companyId?: string | null; } +function runtimeCompanyParams( + params: { companyId?: string | null } | undefined, + fallbackCompanyId: string | null | undefined, +): { companyId?: string | null } { + if (params && Object.prototype.hasOwnProperty.call(params, "companyId")) { + return { companyId: params.companyId }; + } + return fallbackCompanyId == null ? {} : { companyId: fallbackCompanyId }; +} + // --------------------------------------------------------------------------- // Internal: event registration // --------------------------------------------------------------------------- @@ -411,6 +421,11 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost // ----------------------------------------------------------------------- function buildContext(): PluginContext { + const currentCompanyId = () => + runtimeCompanyContext.getStore()?.companyId ?? + invocationContextStorage.getStore()?.scope.companyId ?? + null; + return { get manifest() { if (!manifest) throw new Error("Plugin context accessed before initialization"); @@ -419,9 +434,10 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost config: { async get(params) { - const companyId = - params?.companyId ?? runtimeCompanyContext.getStore()?.companyId ?? null; - return callHost("config.get", companyId ? { companyId } : {}); + return callHost("config.get", runtimeCompanyParams( + params, + currentCompanyId(), + )); }, }, @@ -572,8 +588,16 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost secrets: { async resolve(secretRef: string, companyId?: string | null): Promise { - const scopedCompanyId = companyId ?? runtimeCompanyContext.getStore()?.companyId ?? null; - return callHost("secrets.resolve", { secretRef, companyId: scopedCompanyId }); + const providedCompanyParams = arguments.length > 1 + ? { companyId } + : undefined; + return callHost("secrets.resolve", { + secretRef, + ...runtimeCompanyParams( + providedCompanyParams, + currentCompanyId(), + ), + }); }, }, @@ -1779,7 +1803,21 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost if (notif.method === "agents.sessions.event" && notif.params) { const event = notif.params as AgentSessionEvent; const cb = sessionEventCallbacks.get(event.sessionId); - if (cb) cb(event); + if (cb) { + Promise.resolve( + runNotification(() => + runtimeCompanyContext.run( + { companyId: event.companyId ?? null }, + () => cb(event), + ), + ), + ).catch((err) => { + notifyHost("log", { + level: "error", + message: `Failed to handle agent session event: ${err instanceof Error ? err.message : String(err)}`, + }); + }); + } } else if (notif.method === "onEvent" && notif.params) { // Plugin event bus notifications — dispatch to registered event handlers Promise.resolve(runNotification(() => handleOnEvent(notif.params as OnEventParams))).catch((err) => { diff --git a/packages/plugins/sdk/tests/worker-rpc-host.test.ts b/packages/plugins/sdk/tests/worker-rpc-host.test.ts index 1ed698b1..7d0a1e0c 100644 --- a/packages/plugins/sdk/tests/worker-rpc-host.test.ts +++ b/packages/plugins/sdk/tests/worker-rpc-host.test.ts @@ -354,66 +354,273 @@ describe("startWorkerRpcHost runtime company context", () => { }); const host = startWorkerRpcHost({ plugin, stdin, stdout }); + try { + writeMessage(stdin, { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + manifest: { id: "test-plugin", name: "test-plugin", version: "1.0.0" }, + config: {}, + instanceInfo: { instanceId: "inst-1", hostVersion: "0.0.0-test" }, + apiVersion: 1, + }, + }); + await expect(nextMessage()).resolves.toMatchObject({ id: 1, result: { ok: true } }); - writeMessage(stdin, { - jsonrpc: "2.0", - id: 1, - method: "initialize", - params: { - manifest: { id: "test-plugin", name: "test-plugin", version: "1.0.0" }, - config: {}, - instanceInfo: { instanceId: "inst-1", hostVersion: "0.0.0-test" }, - apiVersion: 1, + writeMessage(stdin, { + jsonrpc: "2.0", + id: 2, + method: "executeTool", + params: { + toolName: "check-context", + parameters: {}, + runContext: { + agentId: "agent-1", + runId: "run-1", + companyId: "company-1", + projectId: "project-1", + }, + }, + }); + + const configRequest = await nextMessage(); + expect(configRequest).toMatchObject({ + method: "config.get", + params: { companyId: "company-1" }, + }); + writeMessage(stdin, { + jsonrpc: "2.0", + id: configRequest.id, + result: { mode: "company-config" }, + }); + + const secretRequest = await nextMessage(); + expect(secretRequest).toMatchObject({ + method: "secrets.resolve", + params: { + secretRef: "77777777-7777-4777-8777-777777777777", + companyId: "company-1", + }, + }); + writeMessage(stdin, { + jsonrpc: "2.0", + id: secretRequest.id, + result: "company-secret", + }); + + await expect(nextMessage()).resolves.toMatchObject({ + id: 2, + result: { content: "company-config:company-secret" }, + }); + } finally { + host.stop(); + stdin.end(); + stdout.destroy(); + } + }); + + it("preserves explicit null company context overrides", async () => { + const stdin = new PassThrough(); + const stdout = new PassThrough(); + const nextMessage = collectJsonLines(stdout); + + const plugin = definePlugin({ + async setup(ctx) { + ctx.tools.register( + "clear-context", + { + displayName: "Clear Context", + description: "Checks explicit null company override propagation", + parametersSchema: { type: "object", properties: {} }, + }, + async () => { + const config = await ctx.config.get({ companyId: null }); + const token = await ctx.secrets.resolve( + "77777777-7777-4777-8777-777777777777", + null, + ); + return { content: `${config.mode}:${token}` }; + }, + ); }, }); - await expect(nextMessage()).resolves.toMatchObject({ id: 1, result: { ok: true } }); - writeMessage(stdin, { - jsonrpc: "2.0", - id: 2, - method: "executeTool", - params: { - toolName: "check-context", - parameters: {}, - runContext: { - agentId: "agent-1", + const host = startWorkerRpcHost({ plugin, stdin, stdout }); + try { + writeMessage(stdin, { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + manifest: { id: "test-plugin", name: "test-plugin", version: "1.0.0" }, + config: {}, + instanceInfo: { instanceId: "inst-1", hostVersion: "0.0.0-test" }, + apiVersion: 1, + }, + }); + await expect(nextMessage()).resolves.toMatchObject({ id: 1, result: { ok: true } }); + + writeMessage(stdin, { + jsonrpc: "2.0", + id: 2, + method: "executeTool", + params: { + toolName: "clear-context", + parameters: {}, + runContext: { + agentId: "agent-1", + runId: "run-1", + companyId: "company-1", + projectId: "project-1", + }, + }, + }); + + const configRequest = await nextMessage(); + expect(configRequest).toMatchObject({ + method: "config.get", + params: { companyId: null }, + }); + writeMessage(stdin, { + jsonrpc: "2.0", + id: configRequest.id, + result: { mode: "global-config" }, + }); + + const secretRequest = await nextMessage(); + expect(secretRequest).toMatchObject({ + method: "secrets.resolve", + params: { + secretRef: "77777777-7777-4777-8777-777777777777", + companyId: null, + }, + }); + writeMessage(stdin, { + jsonrpc: "2.0", + id: secretRequest.id, + result: "global-secret", + }); + + await expect(nextMessage()).resolves.toMatchObject({ + id: 2, + result: { content: "global-config:global-secret" }, + }); + } finally { + host.stop(); + stdin.end(); + stdout.destroy(); + } + }); + + it("passes session event company context into config host calls", async () => { + const stdin = new PassThrough(); + const stdout = new PassThrough(); + const nextMessage = collectJsonLines(stdout); + + const plugin = definePlugin({ + async setup(ctx) { + ctx.data.register("start-session", async () => { + await ctx.agents.sessions.sendMessage("session-1", "company-1", { + prompt: "hello", + onEvent: async () => { + await ctx.config.get(); + }, + }); + return { ok: true }; + }); + }, + }); + + const host = startWorkerRpcHost({ plugin, stdin, stdout }); + try { + writeMessage(stdin, { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + manifest: { + id: "test-plugin", + name: "test-plugin", + version: "1.0.0", + capabilities: ["agent.sessions.send"], + }, + config: {}, + instanceInfo: { instanceId: "inst-1", hostVersion: "0.0.0-test" }, + apiVersion: 1, + }, + }); + await expect(nextMessage()).resolves.toMatchObject({ id: 1, result: { ok: true } }); + + writeMessage(stdin, { + jsonrpc: "2.0", + id: 2, + method: "getData", + params: { + key: "start-session", + companyId: "company-1", + params: {}, + }, + paperclipInvocation: { + id: "invocation-session", + scope: { companyId: "company-1" }, + }, + }); + + const sendMessageRequest = await nextMessage(); + expect(sendMessageRequest).toMatchObject({ + method: "agents.sessions.sendMessage", + params: { + sessionId: "session-1", + companyId: "company-1", + prompt: "hello", + }, + }); + writeMessage(stdin, { + jsonrpc: "2.0", + id: sendMessageRequest.id, + result: { runId: "run-1" }, + }); + + await expect(nextMessage()).resolves.toMatchObject({ + id: 2, + result: { ok: true }, + }); + + writeMessage(stdin, { + jsonrpc: "2.0", + method: "agents.sessions.event", + params: { + sessionId: "session-1", runId: "run-1", companyId: "company-1", - projectId: "project-1", + seq: 1, + eventType: "chunk", + stream: "stdout", + message: "hello", + payload: null, }, - }, - }); + paperclipInvocation: { + id: "invocation-session", + scope: { companyId: "company-1" }, + }, + }); - const configRequest = await nextMessage(); - expect(configRequest).toMatchObject({ - method: "config.get", - params: { companyId: "company-1" }, - }); - writeMessage(stdin, { - jsonrpc: "2.0", - id: configRequest.id, - result: { mode: "company-config" }, - }); - - const secretRequest = await nextMessage(); - expect(secretRequest).toMatchObject({ - method: "secrets.resolve", - params: { - secretRef: "77777777-7777-4777-8777-777777777777", - companyId: "company-1", - }, - }); - writeMessage(stdin, { - jsonrpc: "2.0", - id: secretRequest.id, - result: "company-secret", - }); - - await expect(nextMessage()).resolves.toMatchObject({ - id: 2, - result: { content: "company-config:company-secret" }, - }); - - host.stop(); + const configRequest = await nextMessage(); + expect(configRequest).toMatchObject({ + method: "config.get", + params: { companyId: "company-1" }, + paperclipInvocationId: "invocation-session", + }); + writeMessage(stdin, { + jsonrpc: "2.0", + id: configRequest.id, + result: { mode: "company-config" }, + }); + } finally { + host.stop(); + stdin.end(); + stdout.destroy(); + } }); }); diff --git a/server/src/__tests__/plugin-host-services-session-events.test.ts b/server/src/__tests__/plugin-host-services-session-events.test.ts new file mode 100644 index 00000000..91ffe4d0 --- /dev/null +++ b/server/src/__tests__/plugin-host-services-session-events.test.ts @@ -0,0 +1,129 @@ +import { randomUUID } from "node:crypto"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { agentTaskSessions, agents, companies, createDb } from "@paperclipai/db"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; + +let liveEventListener: ((event: { + type: string; + payload?: Record; +}) => void) | null = null; + +vi.mock("../services/heartbeat.js", () => ({ + heartbeatService: () => ({ + wakeup: vi.fn(async () => ({ id: "run-1" })), + }), +})); + +vi.mock("../services/live-events.js", () => ({ + subscribeCompanyLiveEvents: vi.fn((_companyId: string, listener: typeof liveEventListener) => { + liveEventListener = listener; + return () => { + liveEventListener = null; + }; + }), +})); + +const { buildHostServices } = await import("../services/plugin-host-services.js"); + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +describeEmbeddedPostgres("plugin host services session events", () => { + let db!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-plugin-host-services-"); + db = createDb(tempDb.connectionString); + }, 20_000); + + afterEach(async () => { + liveEventListener = null; + await db.delete(agentTaskSessions); + await db.delete(agents); + await db.delete(companies); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + function createEventBusStub() { + return { + forPlugin() { + return { + emit: async () => {}, + subscribe: () => {}, + clear: () => {}, + }; + }, + } as any; + } + + async function seedCompanyAndAgent() { + const companyId = randomUUID(); + const agentId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await db.insert(agents).values({ + id: agentId, + companyId, + name: "Engineer", + role: "engineer", + status: "idle", + adapterType: "process", + adapterConfig: { command: "true" }, + runtimeConfig: {}, + permissions: {}, + }); + return { companyId, agentId }; + } + + it("forwards companyId on live agent session notifications", async () => { + const { companyId, agentId } = await seedCompanyAndAgent(); + const notifyWorker = vi.fn(); + const services = buildHostServices( + db, + "plugin-record-id", + "paperclip.missions", + createEventBusStub(), + notifyWorker, + ); + + const session = await services.agentSessions.create({ agentId, companyId }); + const sendResult = await services.agentSessions.sendMessage({ + sessionId: session.sessionId, + companyId, + prompt: "hello", + }); + + liveEventListener?.({ + type: "heartbeat.run.log", + payload: { + runId: sendResult.runId, + seq: 1, + stream: "stdout", + chunk: "hello from run", + }, + }); + + expect(notifyWorker).toHaveBeenCalledWith( + "agents.sessions.event", + expect.objectContaining({ + sessionId: session.sessionId, + runId: sendResult.runId, + companyId, + eventType: "chunk", + }), + ); + + services.dispose(); + }); +}); diff --git a/server/src/__tests__/plugin-routes-authz.test.ts b/server/src/__tests__/plugin-routes-authz.test.ts index d7199191..2eb8f94c 100644 --- a/server/src/__tests__/plugin-routes-authz.test.ts +++ b/server/src/__tests__/plugin-routes-authz.test.ts @@ -86,7 +86,9 @@ async function createApp( next(); }); app.use("/api", pluginRoutes( - (routeOverrides.db ?? {}) as never, + (routeOverrides.db ?? { + transaction: async (callback: (tx: unknown) => Promise) => callback({}), + }) as never, loader as never, routeOverrides.jobDeps as never, undefined, @@ -363,7 +365,9 @@ describe.sequential("plugin install and upgrade authz", () => { const { app } = await createApp(boardActor({ isInstanceAdmin: true, companyIds: [companyA], - })); + }), {}, { + bridgeDeps: { workerManager: mockWorkerManager }, + }); const res = await request(app) .post(`/api/plugins/${pluginId}/config`) @@ -382,6 +386,7 @@ describe.sequential("plugin install and upgrade authz", () => { secretId: "77777777-7777-4777-8777-777777777777", configPath: "$", }], + { db: {} }, ); expect(mockRegistry.upsertConfig).toHaveBeenCalledWith( pluginId, @@ -392,6 +397,7 @@ describe.sequential("plugin install and upgrade authz", () => { }, companyA, ); + expect(mockWorkerManager.call).not.toHaveBeenCalled(); }, 20_000); it("rejects company-scoped plugin config secret refs that do not belong to the selected company", async () => { diff --git a/server/src/routes/plugins.ts b/server/src/routes/plugins.ts index 647df8a9..99a40826 100644 --- a/server/src/routes/plugins.ts +++ b/server/src/routes/plugins.ts @@ -79,6 +79,8 @@ import { import { secretService } from "../services/secrets.js"; import { badRequest, forbidden, notFound, unauthorized, unprocessable } from "../errors.js"; +type DbTransaction = Parameters[0]>[0]; + /** UI slot declaration extracted from plugin manifest */ type PluginUiSlotDeclaration = NonNullable["slots"]>[number]; /** Launcher declaration extracted from plugin manifest */ @@ -691,6 +693,30 @@ export function pluginRoutes( return null; } + async function upsertPluginConfigWithSecretRefs( + pluginId: string, + configJson: Record, + companyId: string | null, + secretRefsByPath: Map>, + ) { + return db.transaction(async (tx) => { + const txRegistry = pluginRegistryService(tx as unknown as Db); + if (companyId) { + const refs = [...secretRefsByPath.entries()].flatMap(([secretId, paths]) => + [...paths].map((configPath) => ({ secretId, configPath })), + ); + await secrets.syncSecretRefsForTarget( + companyId, + { targetType: "plugin", targetId: pluginId }, + refs, + { db: tx as DbTransaction }, + ); + } + + return txRegistry.upsertConfig(pluginId, { configJson }, companyId); + }); + } + function assertPluginBridgeScope(req: Request, companyId: unknown): string | undefined { if (companyId === undefined || companyId === null) { assertInstanceAdmin(req); @@ -2201,20 +2227,12 @@ export function pluginRoutes( res.status(422).json({ error: "Plugin secret references require companyId" }); return; } - if (companyId) { - const refs = [...secretRefsByPath.entries()].flatMap(([secretId, paths]) => - [...paths].map((configPath) => ({ secretId, configPath })), - ); - await secrets.syncSecretRefsForTarget( - companyId, - { targetType: "plugin", targetId: plugin.id }, - refs, - ); - } - - const result = await registry.upsertConfig(plugin.id, { - configJson: body.configJson, - }, companyId); + const result = await upsertPluginConfigWithSecretRefs( + plugin.id, + body.configJson, + companyId, + secretRefsByPath, + ); await logPluginMutationActivity(req, "plugin.config.updated", plugin.id, { pluginId: plugin.id, pluginKey: plugin.pluginKey, diff --git a/server/src/services/plugin-host-services.ts b/server/src/services/plugin-host-services.ts index 4ea90454..d7ef78c0 100644 --- a/server/src/services/plugin-host-services.ts +++ b/server/src/services/plugin-host-services.ts @@ -2647,6 +2647,7 @@ export function buildHostServices( notifyWorker("agents.sessions.event", { sessionId: params.sessionId, runId: run.id, + companyId, seq: (payload.seq as number) ?? 0, eventType: "chunk", stream: (payload.stream as string) ?? null, @@ -2659,6 +2660,7 @@ export function buildHostServices( notifyWorker("agents.sessions.event", { sessionId: params.sessionId, runId: run.id, + companyId, seq: 0, eventType: status === "succeeded" ? "done" : "error", stream: "system", @@ -2670,6 +2672,7 @@ export function buildHostServices( notifyWorker("agents.sessions.event", { sessionId: params.sessionId, runId: run.id, + companyId, seq: 0, eventType: "status", stream: "system", diff --git a/server/src/services/plugin-registry.ts b/server/src/services/plugin-registry.ts index e2150301..83d8cd63 100644 --- a/server/src/services/plugin-registry.ts +++ b/server/src/services/plugin-registry.ts @@ -1,4 +1,4 @@ -import { asc, eq, ne, sql, and, isNull } from "drizzle-orm"; +import { asc, eq, ne, sql, and, isNull, or } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { plugins, @@ -44,13 +44,27 @@ function isPluginKeyConflict(error: unknown): boolean { return err.code === "23505" && constraint === "plugins_plugin_key_idx"; } -function pluginConfigScopeCondition(pluginId: string, companyId?: string | null) { +function pluginConfigExactScopeCondition(pluginId: string, companyId?: string | null) { return and( eq(pluginConfig.pluginId, pluginId), companyId ? eq(pluginConfig.companyId, companyId) : isNull(pluginConfig.companyId), ); } +function pluginConfigReadScopeCondition(pluginId: string, companyId?: string | null) { + if (!companyId) { + return pluginConfigExactScopeCondition(pluginId, null); + } + + return and( + eq(pluginConfig.pluginId, pluginId), + or( + eq(pluginConfig.companyId, companyId), + isNull(pluginConfig.companyId), + ), + ); +} + // --------------------------------------------------------------------------- // Service // --------------------------------------------------------------------------- @@ -292,7 +306,12 @@ export function pluginRegistryService(db: Db) { db .select() .from(pluginConfig) - .where(pluginConfigScopeCondition(pluginId, companyId)) + .where(pluginConfigReadScopeCondition(pluginId, companyId)) + .orderBy( + companyId + ? sql`case when ${pluginConfig.companyId} = ${companyId} then 0 else 1 end` + : sql`0`, + ) .then((rows) => rows[0] ?? null), /** @@ -307,7 +326,7 @@ export function pluginRegistryService(db: Db) { const existing = await db .select() .from(pluginConfig) - .where(pluginConfigScopeCondition(pluginId, companyId)) + .where(pluginConfigExactScopeCondition(pluginId, companyId)) .then((rows) => rows[0] ?? null); if (existing) { @@ -318,7 +337,7 @@ export function pluginRegistryService(db: Db) { lastError: null, updatedAt: new Date(), }) - .where(pluginConfigScopeCondition(pluginId, companyId)) + .where(pluginConfigExactScopeCondition(pluginId, companyId)) .returning() .then((rows) => rows[0]); } @@ -345,7 +364,7 @@ export function pluginRegistryService(db: Db) { const existing = await db .select() .from(pluginConfig) - .where(pluginConfigScopeCondition(pluginId, companyId)) + .where(pluginConfigExactScopeCondition(pluginId, companyId)) .then((rows) => rows[0] ?? null); if (existing) { @@ -357,7 +376,7 @@ export function pluginRegistryService(db: Db) { lastError: null, updatedAt: new Date(), }) - .where(pluginConfigScopeCondition(pluginId, companyId)) + .where(pluginConfigExactScopeCondition(pluginId, companyId)) .returning() .then((rows) => rows[0]); } @@ -381,7 +400,7 @@ export function pluginRegistryService(db: Db) { const rows = await db .update(pluginConfig) .set({ lastError, updatedAt: new Date() }) - .where(pluginConfigScopeCondition(pluginId, companyId)) + .where(pluginConfigExactScopeCondition(pluginId, companyId)) .returning(); if (rows.length === 0) throw notFound("Plugin config not found"); @@ -392,7 +411,7 @@ export function pluginRegistryService(db: Db) { deleteConfig: async (pluginId: string, companyId?: string | null) => { const rows = await db .delete(pluginConfig) - .where(pluginConfigScopeCondition(pluginId, companyId)) + .where(pluginConfigExactScopeCondition(pluginId, companyId)) .returning(); return rows[0] ?? null;