mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 06:20:55 +00:00
fix(gateway): fail closed on invalid subagent allowlists
This commit is contained in:
@@ -322,6 +322,38 @@ describe("loadGatewayPlugins", () => {
|
||||
expect(getLastDispatchedParams()).not.toHaveProperty("provider");
|
||||
});
|
||||
|
||||
test("rejects trusted fallback overrides when the configured allowlist normalizes to empty", async () => {
|
||||
const serverPlugins = await importServerPluginsModule();
|
||||
const runtime = await createSubagentRuntime(serverPlugins, {
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: ["anthropic"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
serverPlugins.setFallbackGatewayContext(createTestContext("fallback-invalid-allowlist"));
|
||||
const gatewayScopeModule = await import("../plugins/runtime/gateway-request-scope.js");
|
||||
|
||||
await expect(
|
||||
gatewayScopeModule.withPluginRuntimePluginIdScope("voice-call", () =>
|
||||
runtime.run({
|
||||
sessionKey: "s-invalid-allowlist",
|
||||
message: "use trusted override",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-5",
|
||||
deliver: false,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'plugin "voice-call" configured subagent.allowedModels, but none of the entries normalized to a valid provider/model target.',
|
||||
);
|
||||
});
|
||||
|
||||
test("uses least-privilege synthetic fallback scopes without admin", async () => {
|
||||
const serverPlugins = await importServerPluginsModule();
|
||||
const runtime = await createSubagentRuntime(serverPlugins);
|
||||
|
||||
@@ -52,6 +52,7 @@ export function setFallbackGatewayContext(ctx: GatewayRequestContext): void {
|
||||
type PluginSubagentOverridePolicy = {
|
||||
allowModelOverride: boolean;
|
||||
allowAnyModel: boolean;
|
||||
hasConfiguredAllowlist: boolean;
|
||||
allowedModels: Set<string>;
|
||||
};
|
||||
|
||||
@@ -104,6 +105,7 @@ function setPluginSubagentOverridePolicies(cfg: ReturnType<typeof loadConfig>):
|
||||
const policies: PluginSubagentPolicyState["policies"] = {};
|
||||
for (const [pluginId, entry] of Object.entries(normalized.entries)) {
|
||||
const allowModelOverride = entry.subagent?.allowModelOverride === true;
|
||||
const hasConfiguredAllowlist = entry.subagent?.hasAllowedModelsConfig === true;
|
||||
const configuredAllowedModels = entry.subagent?.allowedModels ?? [];
|
||||
const allowedModels = new Set<string>();
|
||||
let allowAnyModel = false;
|
||||
@@ -118,12 +120,18 @@ function setPluginSubagentOverridePolicies(cfg: ReturnType<typeof loadConfig>):
|
||||
}
|
||||
allowedModels.add(normalizedModelRef);
|
||||
}
|
||||
if (!allowModelOverride && allowedModels.size === 0 && !allowAnyModel) {
|
||||
if (
|
||||
!allowModelOverride &&
|
||||
!hasConfiguredAllowlist &&
|
||||
allowedModels.size === 0 &&
|
||||
!allowAnyModel
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
policies[pluginId] = {
|
||||
allowModelOverride,
|
||||
allowAnyModel,
|
||||
hasConfiguredAllowlist,
|
||||
allowedModels,
|
||||
};
|
||||
}
|
||||
@@ -149,7 +157,16 @@ function authorizeFallbackModelOverride(params: {
|
||||
reason: `plugin "${pluginId}" is not trusted for fallback provider/model override requests.`,
|
||||
};
|
||||
}
|
||||
if (policy.allowAnyModel || policy.allowedModels.size === 0) {
|
||||
if (policy.allowAnyModel) {
|
||||
return { allowed: true };
|
||||
}
|
||||
if (policy.hasConfiguredAllowlist && policy.allowedModels.size === 0) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `plugin "${pluginId}" configured subagent.allowedModels, but none of the entries normalized to a valid provider/model target.`,
|
||||
};
|
||||
}
|
||||
if (policy.allowedModels.size === 0) {
|
||||
return { allowed: true };
|
||||
}
|
||||
const requestedModelRef = resolveRequestedFallbackModelRef(params);
|
||||
|
||||
@@ -91,11 +91,30 @@ describe("normalizePluginsConfig", () => {
|
||||
});
|
||||
expect(result.entries["voice-call"]?.subagent).toEqual({
|
||||
allowModelOverride: true,
|
||||
hasAllowedModelsConfig: true,
|
||||
allowedModels: ["anthropic/claude-haiku-4-5", "openai/gpt-4.1-mini"],
|
||||
});
|
||||
});
|
||||
|
||||
it("drops invalid plugin subagent override policy values", () => {
|
||||
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": {
|
||||
@@ -106,7 +125,9 @@ describe("normalizePluginsConfig", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.entries["voice-call"]?.subagent).toBeUndefined();
|
||||
expect(result.entries["voice-call"]?.subagent).toEqual({
|
||||
hasAllowedModelsConfig: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes legacy plugin ids to their merged bundled plugin id", () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ export type NormalizedPluginsConfig = {
|
||||
subagent?: {
|
||||
allowModelOverride?: boolean;
|
||||
allowedModels?: string[];
|
||||
hasAllowedModelsConfig?: boolean;
|
||||
};
|
||||
config?: unknown;
|
||||
}
|
||||
@@ -133,6 +134,9 @@ const normalizePluginEntries = (entries: unknown): NormalizedPluginsConfig["entr
|
||||
? {
|
||||
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() : ""))
|
||||
@@ -143,11 +147,13 @@ const normalizePluginEntries = (entries: unknown): NormalizedPluginsConfig["entr
|
||||
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 }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user