Fix plugin company-scoped config resolution

This commit is contained in:
Paperclip Bot 2026-06-03 14:08:15 +00:00
parent 5317029ef4
commit 0509d08f3c
8 changed files with 505 additions and 84 deletions

View file

@ -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";

View file

@ -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) => {

View file

@ -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();
}
});
});