openclaw/src/plugins/runtime/gateway-request-scope.ts
Robin Waslander a1520d70ff
fix(gateway): propagate real gateway client into plugin subagent runtime
Plugin subagent dispatch used a hardcoded synthetic client carrying
operator.admin, operator.approvals, and operator.pairing for all
runtime.subagent.* calls. Plugin HTTP routes with auth:"plugin" require
no gateway auth by design, so an unauthenticated external request could
drive admin-only gateway methods (sessions.delete, agent.run) through
the subagent runtime.

Propagate the real gateway client into the plugin runtime request scope
when one is available. Plugin HTTP routes now run inside a scoped
runtime client: auth:"plugin" routes receive a non-admin synthetic
operator.write client; gateway-authenticated routes retain admin-capable
scopes. The security boundary is enforced at the HTTP handler level.

Fixes GHSA-xw77-45gv-p728
2026-03-11 14:17:01 +01:00

48 lines
1.5 KiB
TypeScript

import { AsyncLocalStorage } from "node:async_hooks";
import type {
GatewayRequestContext,
GatewayRequestOptions,
} from "../../gateway/server-methods/types.js";
export type PluginRuntimeGatewayRequestScope = {
context?: GatewayRequestContext;
client?: GatewayRequestOptions["client"];
isWebchatConnect: GatewayRequestOptions["isWebchatConnect"];
};
const PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY: unique symbol = Symbol.for(
"openclaw.pluginRuntimeGatewayRequestScope",
);
const pluginRuntimeGatewayRequestScope = (() => {
const globalState = globalThis as typeof globalThis & {
[PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY]?: AsyncLocalStorage<PluginRuntimeGatewayRequestScope>;
};
const existing = globalState[PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY];
if (existing) {
return existing;
}
const created = new AsyncLocalStorage<PluginRuntimeGatewayRequestScope>();
globalState[PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY] = created;
return created;
})();
/**
* Runs plugin gateway handlers with request-scoped context that runtime helpers can read.
*/
export function withPluginRuntimeGatewayRequestScope<T>(
scope: PluginRuntimeGatewayRequestScope,
run: () => T,
): T {
return pluginRuntimeGatewayRequestScope.run(scope, run);
}
/**
* Returns the current plugin gateway request scope when called from a plugin request handler.
*/
export function getPluginRuntimeGatewayRequestScope():
| PluginRuntimeGatewayRequestScope
| undefined {
return pluginRuntimeGatewayRequestScope.getStore();
}