mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 15:31:07 +00:00
fix(plugins): forward plugin subagent overrides (#48277)
Merged via squash.
Prepared head SHA: ffa45893e0
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -78,6 +78,58 @@ describe("normalizePluginsConfig", () => {
|
||||
expect(result.entries["voice-call"]?.hooks).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes plugin subagent override policy settings", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
entries: {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: [" anthropic/claude-haiku-4-5 ", "", "openai/gpt-4.1-mini"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.entries["voice-call"]?.subagent).toEqual({
|
||||
allowModelOverride: true,
|
||||
hasAllowedModelsConfig: true,
|
||||
allowedModels: ["anthropic/claude-haiku-4-5", "openai/gpt-4.1-mini"],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves explicit subagent allowlist intent even when all entries are invalid", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
entries: {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: [42, null, "anthropic"],
|
||||
} as unknown as { allowModelOverride: boolean; allowedModels: string[] },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.entries["voice-call"]?.subagent).toEqual({
|
||||
allowModelOverride: true,
|
||||
hasAllowedModelsConfig: true,
|
||||
allowedModels: ["anthropic"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit invalid subagent allowlist config visible to callers", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
entries: {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: "nope",
|
||||
allowedModels: [42, null],
|
||||
} as unknown as { allowModelOverride: boolean; allowedModels: string[] },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.entries["voice-call"]?.subagent).toEqual({
|
||||
hasAllowedModelsConfig: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes legacy plugin ids to their merged bundled plugin id", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
allow: ["openai-codex", "minimax-portal-auth"],
|
||||
|
||||
@@ -18,6 +18,11 @@ export type NormalizedPluginsConfig = {
|
||||
hooks?: {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
subagent?: {
|
||||
allowModelOverride?: boolean;
|
||||
allowedModels?: string[];
|
||||
hasAllowedModelsConfig?: boolean;
|
||||
};
|
||||
config?: unknown;
|
||||
}
|
||||
>;
|
||||
@@ -123,11 +128,43 @@ const normalizePluginEntries = (entries: unknown): NormalizedPluginsConfig["entr
|
||||
allowPromptInjection: hooks.allowPromptInjection,
|
||||
}
|
||||
: undefined;
|
||||
const subagentRaw = entry.subagent;
|
||||
const subagent =
|
||||
subagentRaw && typeof subagentRaw === "object" && !Array.isArray(subagentRaw)
|
||||
? {
|
||||
allowModelOverride: (subagentRaw as { allowModelOverride?: unknown })
|
||||
.allowModelOverride,
|
||||
hasAllowedModelsConfig: Array.isArray(
|
||||
(subagentRaw as { allowedModels?: unknown }).allowedModels,
|
||||
),
|
||||
allowedModels: Array.isArray((subagentRaw as { allowedModels?: unknown }).allowedModels)
|
||||
? ((subagentRaw as { allowedModels?: unknown }).allowedModels as unknown[])
|
||||
.map((model) => (typeof model === "string" ? model.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
}
|
||||
: undefined;
|
||||
const normalizedSubagent =
|
||||
subagent &&
|
||||
(typeof subagent.allowModelOverride === "boolean" ||
|
||||
subagent.hasAllowedModelsConfig ||
|
||||
(Array.isArray(subagent.allowedModels) && subagent.allowedModels.length > 0))
|
||||
? {
|
||||
...(typeof subagent.allowModelOverride === "boolean"
|
||||
? { allowModelOverride: subagent.allowModelOverride }
|
||||
: {}),
|
||||
...(subagent.hasAllowedModelsConfig ? { hasAllowedModelsConfig: true } : {}),
|
||||
...(Array.isArray(subagent.allowedModels) && subagent.allowedModels.length > 0
|
||||
? { allowedModels: subagent.allowedModels }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
normalized[normalizedKey] = {
|
||||
...normalized[normalizedKey],
|
||||
enabled:
|
||||
typeof entry.enabled === "boolean" ? entry.enabled : normalized[normalizedKey]?.enabled,
|
||||
hooks: normalizedHooks ?? normalized[normalizedKey]?.hooks,
|
||||
subagent: normalizedSubagent ?? normalized[normalizedKey]?.subagent,
|
||||
config: "config" in entry ? entry.config : normalized[normalizedKey]?.config,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { normalizePluginHttpPath } from "./http-path.js";
|
||||
import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js";
|
||||
import { registerPluginInteractiveHandler } from "./interactive.js";
|
||||
import { normalizeRegisteredProvider } from "./provider-validation.js";
|
||||
import { withPluginRuntimePluginIdScope } from "./runtime/gateway-request-scope.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { defaultSlotIdForKey } from "./slots.js";
|
||||
import {
|
||||
@@ -835,6 +836,36 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
debug: logger.debug,
|
||||
});
|
||||
|
||||
const pluginRuntimeById = new Map<string, PluginRuntime>();
|
||||
|
||||
const resolvePluginRuntime = (pluginId: string): PluginRuntime => {
|
||||
const cached = pluginRuntimeById.get(pluginId);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const runtime = new Proxy(registryParams.runtime, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop !== "subagent") {
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
const subagent = Reflect.get(target, prop, receiver);
|
||||
return {
|
||||
run: (params) => withPluginRuntimePluginIdScope(pluginId, () => subagent.run(params)),
|
||||
waitForRun: (params) =>
|
||||
withPluginRuntimePluginIdScope(pluginId, () => subagent.waitForRun(params)),
|
||||
getSessionMessages: (params) =>
|
||||
withPluginRuntimePluginIdScope(pluginId, () => subagent.getSessionMessages(params)),
|
||||
getSession: (params) =>
|
||||
withPluginRuntimePluginIdScope(pluginId, () => subagent.getSession(params)),
|
||||
deleteSession: (params) =>
|
||||
withPluginRuntimePluginIdScope(pluginId, () => subagent.deleteSession(params)),
|
||||
} satisfies PluginRuntime["subagent"];
|
||||
},
|
||||
});
|
||||
pluginRuntimeById.set(pluginId, runtime);
|
||||
return runtime;
|
||||
};
|
||||
|
||||
const createApi = (
|
||||
record: PluginRecord,
|
||||
params: {
|
||||
@@ -855,7 +886,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
registrationMode,
|
||||
config: params.config,
|
||||
pluginConfig: params.pluginConfig,
|
||||
runtime: registryParams.runtime,
|
||||
runtime: resolvePluginRuntime(record.id),
|
||||
logger: normalizeLogger(registryParams.logger),
|
||||
registerTool:
|
||||
registrationMode === "full" ? (tool, opts) => registerTool(record, tool, opts) : () => {},
|
||||
|
||||
@@ -20,4 +20,17 @@ describe("gateway request scope", () => {
|
||||
expect(second.getPluginRuntimeGatewayRequestScope()).toEqual(TEST_SCOPE);
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches plugin id to the active scope", async () => {
|
||||
const runtimeScope = await import("./gateway-request-scope.js");
|
||||
|
||||
await runtimeScope.withPluginRuntimeGatewayRequestScope(TEST_SCOPE, async () => {
|
||||
await runtimeScope.withPluginRuntimePluginIdScope("voice-call", async () => {
|
||||
expect(runtimeScope.getPluginRuntimeGatewayRequestScope()).toEqual({
|
||||
...TEST_SCOPE,
|
||||
pluginId: "voice-call",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ export type PluginRuntimeGatewayRequestScope = {
|
||||
context?: GatewayRequestContext;
|
||||
client?: GatewayRequestOptions["client"];
|
||||
isWebchatConnect: GatewayRequestOptions["isWebchatConnect"];
|
||||
pluginId?: string;
|
||||
};
|
||||
|
||||
const PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY: unique symbol = Symbol.for(
|
||||
@@ -37,6 +38,20 @@ export function withPluginRuntimeGatewayRequestScope<T>(
|
||||
return pluginRuntimeGatewayRequestScope.run(scope, run);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs work under the current gateway request scope while attaching plugin identity.
|
||||
*/
|
||||
export function withPluginRuntimePluginIdScope<T>(pluginId: string, run: () => T): T {
|
||||
const current = pluginRuntimeGatewayRequestScope.getStore();
|
||||
const scoped: PluginRuntimeGatewayRequestScope = current
|
||||
? { ...current, pluginId }
|
||||
: {
|
||||
pluginId,
|
||||
isWebchatConnect: () => false,
|
||||
};
|
||||
return pluginRuntimeGatewayRequestScope.run(scoped, run);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current plugin gateway request scope when called from a plugin request handler.
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,8 @@ export type { RuntimeLogger };
|
||||
export type SubagentRunParams = {
|
||||
sessionKey: string;
|
||||
message: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
extraSystemPrompt?: string;
|
||||
lane?: string;
|
||||
deliver?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user