feat(plugin): scope secret-ref config by company

This commit is contained in:
Paperclip Bot 2026-06-03 06:31:01 +00:00 committed by Alkim Ake Gozen
parent 62863126a3
commit db0ef46900
19 changed files with 587 additions and 102 deletions

View file

@ -100,7 +100,7 @@ export class InvocationScopeDeniedError extends Error {
export interface HostServices {
/** Provides `config.get`. */
config: {
get(): Promise<Record<string, unknown>>;
get(params: WorkerToHostMethods["config.get"][0]): Promise<Record<string, unknown>>;
};
/** Provides trusted company-scoped local folder helpers. */
@ -627,8 +627,8 @@ export function createHostClientHandlers(
return {
// Config
"config.get": gated("config.get", async () => {
return services.config.get();
"config.get": gated("config.get", async (params) => {
return services.config.get(params);
}),
"localFolders.declarations": gated("localFolders.declarations", async (params) => {

View file

@ -672,7 +672,7 @@ export const HOST_TO_WORKER_OPTIONAL_METHODS: readonly HostToWorkerMethodName[]
*/
export interface WorkerToHostMethods {
// Config
"config.get": [params: Record<string, never>, result: Record<string, unknown>];
"config.get": [params: { companyId?: string | null }, result: Record<string, unknown>];
// Trusted local folders
"localFolders.declarations": [
@ -809,7 +809,7 @@ export interface WorkerToHostMethods {
// Secrets
"secrets.resolve": [
params: { secretRef: string },
params: { secretRef: string; companyId?: string | null },
result: string,
];

View file

@ -425,7 +425,7 @@ export interface PluginConfigClient {
* Values are validated against the plugin's `instanceConfigSchema` by the
* host before being passed to the worker.
*/
get(): Promise<Record<string, unknown>>;
get(params?: { companyId?: string | null }): Promise<Record<string, unknown>>;
}
export interface PluginLocalFolderProblem {
@ -656,7 +656,7 @@ export interface PluginSecretsClient {
* @param secretRef - The secret reference string from plugin config
* @returns The resolved secret value
*/
resolve(secretRef: string): Promise<string>;
resolve(secretRef: string, companyId?: string | null): Promise<string>;
}
/**

View file

@ -164,6 +164,10 @@ export interface WorkerRpcHost {
stop(): void;
}
interface RuntimeCompanyContext {
companyId?: string | null;
}
// ---------------------------------------------------------------------------
// Internal: event registration
// ---------------------------------------------------------------------------
@ -285,6 +289,7 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
let currentConfig: Record<string, unknown> = {};
let databaseNamespace: string | null = null;
const invocationContextStorage = new AsyncLocalStorage<PluginInvocationContext>();
const runtimeCompanyContext = new AsyncLocalStorage<RuntimeCompanyContext>();
// Plugin handler registrations (populated during setup())
const eventHandlers: EventRegistration[] = [];
@ -413,8 +418,10 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
},
config: {
async get() {
return callHost("config.get", {} as Record<string, never>);
async get(params) {
const companyId =
params?.companyId ?? runtimeCompanyContext.getStore()?.companyId ?? null;
return callHost("config.get", companyId ? { companyId } : {});
},
},
@ -564,8 +571,9 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
},
secrets: {
async resolve(secretRef: string): Promise<string> {
return callHost("secrets.resolve", { secretRef });
async resolve(secretRef: string, companyId?: string | null): Promise<string> {
const scopedCompanyId = companyId ?? runtimeCompanyContext.getStore()?.companyId ?? null;
return callHost("secrets.resolve", { secretRef, companyId: scopedCompanyId });
},
},
@ -1467,7 +1475,10 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
if (registration.filter && !allowsEvent(registration.filter, event)) continue;
try {
await registration.fn(event);
await runtimeCompanyContext.run(
{ companyId: event.companyId },
() => registration.fn(event),
);
} catch (err) {
// Log error but continue processing other handlers so one failing
// handler doesn't prevent the rest from running.
@ -1507,7 +1518,10 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
{ code: PLUGIN_RPC_ERROR_CODES.METHOD_NOT_IMPLEMENTED },
);
}
return plugin.definition.onApiRequest(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onApiRequest!(params),
);
}
async function handleGetData(params: GetDataParams): Promise<unknown> {
@ -1515,11 +1529,14 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
if (!handler) {
throw new Error(`No data handler registered for key "${params.key}"`);
}
return handler({
...params.params,
...(params.companyId === undefined ? {} : { companyId: params.companyId }),
...(params.renderEnvironment === undefined ? {} : { renderEnvironment: params.renderEnvironment }),
});
const handlerParams =
params.renderEnvironment === undefined
? params.params
: { ...params.params, renderEnvironment: params.renderEnvironment };
return runtimeCompanyContext.run(
{ companyId: params.companyId ?? null },
() => handler(handlerParams),
);
}
function stringOrNull(value: unknown): string | null {
@ -1552,13 +1569,14 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
if (!handler) {
throw new Error(`No action handler registered for key "${params.key}"`);
}
return handler(
{
...params.params,
...(params.companyId === undefined ? {} : { companyId: params.companyId }),
...(params.renderEnvironment === undefined ? {} : { renderEnvironment: params.renderEnvironment }),
},
actionContextFromParams(params),
const handlerParams =
params.renderEnvironment === undefined
? params.params
: { ...params.params, renderEnvironment: params.renderEnvironment };
const actionContext = actionContextFromParams(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId ?? actionContext.companyId },
() => handler(handlerParams ?? {}, actionContext),
);
}
@ -1567,7 +1585,10 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
if (!entry) {
throw new Error(`No tool handler registered for "${params.toolName}"`);
}
return entry.fn(params.parameters, params.runContext);
return runtimeCompanyContext.run(
{ companyId: params.runContext.companyId },
() => entry.fn(params.parameters, params.runContext),
);
}
function methodNotImplemented(method: string): Error & { code: number } {
@ -1590,49 +1611,70 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
if (!plugin.definition.onEnvironmentProbe) {
throw methodNotImplemented("environmentProbe");
}
return plugin.definition.onEnvironmentProbe(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onEnvironmentProbe!(params),
);
}
async function handleEnvironmentAcquireLease(params: PluginEnvironmentAcquireLeaseParams) {
if (!plugin.definition.onEnvironmentAcquireLease) {
throw methodNotImplemented("environmentAcquireLease");
}
return plugin.definition.onEnvironmentAcquireLease(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onEnvironmentAcquireLease!(params),
);
}
async function handleEnvironmentResumeLease(params: PluginEnvironmentResumeLeaseParams) {
if (!plugin.definition.onEnvironmentResumeLease) {
throw methodNotImplemented("environmentResumeLease");
}
return plugin.definition.onEnvironmentResumeLease(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onEnvironmentResumeLease!(params),
);
}
async function handleEnvironmentReleaseLease(params: PluginEnvironmentReleaseLeaseParams) {
if (!plugin.definition.onEnvironmentReleaseLease) {
throw methodNotImplemented("environmentReleaseLease");
}
return plugin.definition.onEnvironmentReleaseLease(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onEnvironmentReleaseLease!(params),
);
}
async function handleEnvironmentDestroyLease(params: PluginEnvironmentDestroyLeaseParams) {
if (!plugin.definition.onEnvironmentDestroyLease) {
throw methodNotImplemented("environmentDestroyLease");
}
return plugin.definition.onEnvironmentDestroyLease(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onEnvironmentDestroyLease!(params),
);
}
async function handleEnvironmentRealizeWorkspace(params: PluginEnvironmentRealizeWorkspaceParams) {
if (!plugin.definition.onEnvironmentRealizeWorkspace) {
throw methodNotImplemented("environmentRealizeWorkspace");
}
return plugin.definition.onEnvironmentRealizeWorkspace(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onEnvironmentRealizeWorkspace!(params),
);
}
async function handleEnvironmentExecute(params: PluginEnvironmentExecuteParams) {
if (!plugin.definition.onEnvironmentExecute) {
throw methodNotImplemented("environmentExecute");
}
return plugin.definition.onEnvironmentExecute(params);
return runtimeCompanyContext.run(
{ companyId: params.companyId },
() => plugin.definition.onEnvironmentExecute!(params),
);
}
// -----------------------------------------------------------------------