mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-21 06:51:01 +00:00
fix(gateway): preserve trusted subagent cleanup
This commit is contained in:
@@ -789,7 +789,7 @@ describe("agentCommand", () => {
|
||||
const parseModelRefSpy = vi.spyOn(modelSelectionModule, "parseModelRef");
|
||||
parseModelRefSpy.mockImplementationOnce(() => ({
|
||||
provider: "anthropic\u001b[31m",
|
||||
model: "claude-haiku-4-6\u001b[32m",
|
||||
model: "claude-haiku-4-5\u001b[32m",
|
||||
}));
|
||||
try {
|
||||
await withTempHome(async (home) => {
|
||||
@@ -805,12 +805,12 @@ describe("agentCommand", () => {
|
||||
{
|
||||
message: "use disallowed override",
|
||||
sessionKey: "agent:main:subagent:sanitized-override-error",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
},
|
||||
runtime,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Model override "anthropic/claude-haiku-4-6" is not allowed for agent "main".',
|
||||
'Model override "anthropic/claude-haiku-4-5" is not allowed for agent "main".',
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("plugins.entries.*.subagent", () => {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: ["anthropic/claude-haiku-4-6"],
|
||||
allowedModels: ["anthropic/claude-haiku-4-5"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -312,7 +312,7 @@ describe("gateway agent handler", () => {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
idempotencyKey: "test-idem-model-override",
|
||||
},
|
||||
{
|
||||
@@ -329,7 +329,7 @@ describe("gateway agent handler", () => {
|
||||
expect(lastCall?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -345,7 +345,7 @@ describe("gateway agent handler", () => {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
idempotencyKey: "test-idem-model-override-write",
|
||||
},
|
||||
{
|
||||
@@ -378,7 +378,7 @@ describe("gateway agent handler", () => {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
idempotencyKey: "test-idem-model-override-internal",
|
||||
},
|
||||
{
|
||||
@@ -398,7 +398,7 @@ describe("gateway agent handler", () => {
|
||||
expect(lastCall?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
senderIsOwner: false,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -21,6 +21,19 @@ vi.mock("./server-methods.js", () => ({
|
||||
handleGatewayRequest,
|
||||
}));
|
||||
|
||||
vi.mock("../channels/registry.js", () => ({
|
||||
CHAT_CHANNEL_ORDER: [],
|
||||
CHANNEL_IDS: [],
|
||||
listChatChannels: () => [],
|
||||
listChatChannelAliases: () => [],
|
||||
getChatChannelMeta: () => null,
|
||||
normalizeChatChannelId: () => null,
|
||||
normalizeChannelId: () => null,
|
||||
normalizeAnyChannelId: () => null,
|
||||
formatChannelPrimerLine: () => "",
|
||||
formatChannelSelectionLine: () => "",
|
||||
}));
|
||||
|
||||
const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
@@ -210,7 +223,7 @@ describe("loadGatewayPlugins", () => {
|
||||
sessionKey: "s-override",
|
||||
message: "use the override",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
deliver: false,
|
||||
}),
|
||||
);
|
||||
@@ -219,7 +232,7 @@ describe("loadGatewayPlugins", () => {
|
||||
sessionKey: "s-override",
|
||||
message: "use the override",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
deliver: false,
|
||||
});
|
||||
});
|
||||
@@ -234,7 +247,7 @@ describe("loadGatewayPlugins", () => {
|
||||
sessionKey: "s-fallback-override",
|
||||
message: "use the override",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
deliver: false,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
@@ -250,7 +263,7 @@ describe("loadGatewayPlugins", () => {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: ["anthropic/claude-haiku-4-6"],
|
||||
allowedModels: ["anthropic/claude-haiku-4-5"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -264,7 +277,7 @@ describe("loadGatewayPlugins", () => {
|
||||
sessionKey: "s-trusted-override",
|
||||
message: "use trusted override",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
deliver: false,
|
||||
}),
|
||||
);
|
||||
@@ -272,10 +285,43 @@ describe("loadGatewayPlugins", () => {
|
||||
expect(getLastDispatchedParams()).toMatchObject({
|
||||
sessionKey: "s-trusted-override",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
model: "claude-haiku-4-5",
|
||||
});
|
||||
});
|
||||
|
||||
test("allows trusted fallback model-only overrides when the model ref is canonical", async () => {
|
||||
const serverPlugins = await importServerPluginsModule();
|
||||
const runtime = await createSubagentRuntime(serverPlugins, {
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: ["anthropic/claude-haiku-4-5"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
serverPlugins.setFallbackGatewayContext(createTestContext("fallback-model-only-override"));
|
||||
const gatewayScopeModule = await import("../plugins/runtime/gateway-request-scope.js");
|
||||
|
||||
await gatewayScopeModule.withPluginRuntimePluginIdScope("voice-call", () =>
|
||||
runtime.run({
|
||||
sessionKey: "s-model-only-override",
|
||||
message: "use trusted model-only override",
|
||||
model: "anthropic/claude-haiku-4-5",
|
||||
deliver: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getLastDispatchedParams()).toMatchObject({
|
||||
sessionKey: "s-model-only-override",
|
||||
model: "anthropic/claude-haiku-4-5",
|
||||
});
|
||||
expect(getLastDispatchedParams()).not.toHaveProperty("provider");
|
||||
});
|
||||
|
||||
test("uses least-privilege synthetic fallback scopes without admin", async () => {
|
||||
const serverPlugins = await importServerPluginsModule();
|
||||
const runtime = await createSubagentRuntime(serverPlugins);
|
||||
@@ -291,6 +337,19 @@ describe("loadGatewayPlugins", () => {
|
||||
expect(getLastDispatchedClientScopes()).not.toContain("operator.admin");
|
||||
});
|
||||
|
||||
test("keeps admin scope for fallback session deletion", async () => {
|
||||
const serverPlugins = await importServerPluginsModule();
|
||||
const runtime = await createSubagentRuntime(serverPlugins);
|
||||
serverPlugins.setFallbackGatewayContext(createTestContext("synthetic-delete-session"));
|
||||
|
||||
await runtime.deleteSession({
|
||||
sessionKey: "s-delete",
|
||||
deleteTranscript: true,
|
||||
});
|
||||
|
||||
expect(getLastDispatchedClientScopes()).toEqual(["operator.admin"]);
|
||||
});
|
||||
|
||||
test("can prefer setup-runtime channel plugins during startup loads", async () => {
|
||||
const { loadGatewayPlugins } = await importServerPluginsModule();
|
||||
loadOpenClawPlugins.mockReturnValue(createRegistry([]));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { normalizeModelRef } from "../agents/model-selection.js";
|
||||
import { normalizeModelRef, parseModelRef } from "../agents/model-selection.js";
|
||||
import type { loadConfig } from "../config/config.js";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
@@ -149,15 +149,18 @@ function authorizeFallbackModelOverride(params: {
|
||||
reason: `plugin "${pluginId}" is not trusted for fallback provider/model override requests.`,
|
||||
};
|
||||
}
|
||||
if (!params.provider || !params.model) {
|
||||
if (policy.allowAnyModel || policy.allowedModels.size === 0) {
|
||||
return { allowed: true };
|
||||
}
|
||||
const requestedModelRef = resolveRequestedFallbackModelRef(params);
|
||||
if (!requestedModelRef) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "fallback provider/model overrides must include both provider and model values.",
|
||||
reason:
|
||||
"fallback provider/model overrides that use an allowlist must resolve to a canonical provider/model target.",
|
||||
};
|
||||
}
|
||||
const normalizedRequest = normalizeModelRef(params.provider, params.model);
|
||||
const requestedModelRef = `${normalizedRequest.provider}/${normalizedRequest.model}`;
|
||||
if (policy.allowAnyModel || policy.allowedModels.has(requestedModelRef)) {
|
||||
if (policy.allowedModels.has(requestedModelRef)) {
|
||||
return { allowed: true };
|
||||
}
|
||||
return {
|
||||
@@ -166,10 +169,30 @@ function authorizeFallbackModelOverride(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRequestedFallbackModelRef(params: {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}): string | null {
|
||||
if (params.provider && params.model) {
|
||||
const normalizedRequest = normalizeModelRef(params.provider, params.model);
|
||||
return `${normalizedRequest.provider}/${normalizedRequest.model}`;
|
||||
}
|
||||
const rawModel = params.model?.trim();
|
||||
if (!rawModel || !rawModel.includes("/")) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseModelRef(rawModel, "");
|
||||
if (!parsed?.provider || !parsed.model) {
|
||||
return null;
|
||||
}
|
||||
return `${parsed.provider}/${parsed.model}`;
|
||||
}
|
||||
|
||||
// ── Internal gateway dispatch for plugin runtime ────────────────────
|
||||
|
||||
function createSyntheticOperatorClient(params?: {
|
||||
allowModelOverride?: boolean;
|
||||
scopes?: string[];
|
||||
}): GatewayRequestOptions["client"] {
|
||||
return {
|
||||
connect: {
|
||||
@@ -182,7 +205,7 @@ function createSyntheticOperatorClient(params?: {
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
},
|
||||
role: "operator",
|
||||
scopes: [WRITE_SCOPE],
|
||||
scopes: params?.scopes ?? [WRITE_SCOPE],
|
||||
},
|
||||
internal: {
|
||||
allowModelOverride: params?.allowModelOverride === true,
|
||||
@@ -204,6 +227,7 @@ async function dispatchGatewayMethod<T>(
|
||||
params: Record<string, unknown>,
|
||||
options?: {
|
||||
allowSyntheticModelOverride?: boolean;
|
||||
syntheticScopes?: string[];
|
||||
},
|
||||
): Promise<T> {
|
||||
const scope = getPluginRuntimeGatewayRequestScope();
|
||||
@@ -227,6 +251,7 @@ async function dispatchGatewayMethod<T>(
|
||||
scope?.client ??
|
||||
createSyntheticOperatorClient({
|
||||
allowModelOverride: options?.allowSyntheticModelOverride === true,
|
||||
scopes: options?.syntheticScopes,
|
||||
}),
|
||||
isWebchatConnect,
|
||||
respond: (ok, payload, error) => {
|
||||
@@ -321,10 +346,16 @@ function createGatewaySubagentRuntime(): PluginRuntime["subagent"] {
|
||||
return getSessionMessages(params);
|
||||
},
|
||||
async deleteSession(params) {
|
||||
await dispatchGatewayMethod("sessions.delete", {
|
||||
key: params.sessionKey,
|
||||
deleteTranscript: params.deleteTranscript ?? true,
|
||||
});
|
||||
await dispatchGatewayMethod(
|
||||
"sessions.delete",
|
||||
{
|
||||
key: params.sessionKey,
|
||||
deleteTranscript: params.deleteTranscript ?? true,
|
||||
},
|
||||
{
|
||||
syntheticScopes: [ADMIN_SCOPE],
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,14 +84,14 @@ describe("normalizePluginsConfig", () => {
|
||||
"voice-call": {
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: [" anthropic/claude-haiku-4-6 ", "", "openai/gpt-4.1-mini"],
|
||||
allowedModels: [" anthropic/claude-haiku-4-5 ", "", "openai/gpt-4.1-mini"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.entries["voice-call"]?.subagent).toEqual({
|
||||
allowModelOverride: true,
|
||||
allowedModels: ["anthropic/claude-haiku-4-6", "openai/gpt-4.1-mini"],
|
||||
allowedModels: ["anthropic/claude-haiku-4-5", "openai/gpt-4.1-mini"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user