mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 18:30:39 +09:00
Fix plugin company-scoped config resolution
This commit is contained in:
parent
5317029ef4
commit
0509d08f3c
8 changed files with 505 additions and 84 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
129
server/src/__tests__/plugin-host-services-session-events.test.ts
Normal file
129
server/src/__tests__/plugin-host-services-session-events.test.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
}) => 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<typeof createDb>;
|
||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -86,7 +86,9 @@ async function createApp(
|
|||
next();
|
||||
});
|
||||
app.use("/api", pluginRoutes(
|
||||
(routeOverrides.db ?? {}) as never,
|
||||
(routeOverrides.db ?? {
|
||||
transaction: async <T>(callback: (tx: unknown) => Promise<T>) => 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 () => {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ import {
|
|||
import { secretService } from "../services/secrets.js";
|
||||
import { badRequest, forbidden, notFound, unauthorized, unprocessable } from "../errors.js";
|
||||
|
||||
type DbTransaction = Parameters<Parameters<Db["transaction"]>[0]>[0];
|
||||
|
||||
/** UI slot declaration extracted from plugin manifest */
|
||||
type PluginUiSlotDeclaration = NonNullable<NonNullable<PaperclipPluginManifestV1["ui"]>["slots"]>[number];
|
||||
/** Launcher declaration extracted from plugin manifest */
|
||||
|
|
@ -691,6 +693,30 @@ export function pluginRoutes(
|
|||
return null;
|
||||
}
|
||||
|
||||
async function upsertPluginConfigWithSecretRefs(
|
||||
pluginId: string,
|
||||
configJson: Record<string, unknown>,
|
||||
companyId: string | null,
|
||||
secretRefsByPath: Map<string, Set<string>>,
|
||||
) {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue