mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(agents): re-expose configured tools under restrictive profiles
This commit is contained in:
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
filterToolsByPolicy,
|
||||
isToolAllowedByPolicyName,
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveSubagentToolPolicy,
|
||||
} from "./pi-tools.policy.js";
|
||||
import { createStubTool } from "./test-helpers/pi-tool-stubs.js";
|
||||
@@ -176,3 +177,59 @@ describe("resolveSubagentToolPolicy depth awareness", () => {
|
||||
expect(isToolAllowedByPolicyName("sessions_spawn", policy)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEffectiveToolPolicy", () => {
|
||||
it("implicitly re-exposes exec and process when tools.exec is configured", () => {
|
||||
const cfg = {
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
exec: { host: "sandbox" },
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const result = resolveEffectiveToolPolicy({ config: cfg });
|
||||
expect(result.profileAlsoAllow).toEqual(["exec", "process"]);
|
||||
});
|
||||
|
||||
it("implicitly re-exposes read, write, and edit when tools.fs is configured", () => {
|
||||
const cfg = {
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
fs: { workspaceOnly: false },
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const result = resolveEffectiveToolPolicy({ config: cfg });
|
||||
expect(result.profileAlsoAllow).toEqual(["read", "write", "edit"]);
|
||||
});
|
||||
|
||||
it("merges explicit alsoAllow with implicit tool-section exposure", () => {
|
||||
const cfg = {
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
alsoAllow: ["web_search"],
|
||||
exec: { host: "sandbox" },
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const result = resolveEffectiveToolPolicy({ config: cfg });
|
||||
expect(result.profileAlsoAllow).toEqual(["web_search", "exec", "process"]);
|
||||
});
|
||||
|
||||
it("uses agent tool sections when resolving implicit exposure", () => {
|
||||
const cfg = {
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "coder",
|
||||
tools: {
|
||||
fs: { workspaceOnly: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "coder" });
|
||||
expect(result.profileAlsoAllow).toEqual(["read", "write", "edit"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getChannelDock } from "../channels/dock.js";
|
||||
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
|
||||
import type { AgentToolsConfig } from "../config/types.tools.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js";
|
||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
@@ -196,6 +197,37 @@ function resolveProviderToolPolicy(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveExplicitProfileAlsoAllow(tools?: OpenClawConfig["tools"]): string[] | undefined {
|
||||
return Array.isArray(tools?.alsoAllow) ? tools.alsoAllow : undefined;
|
||||
}
|
||||
|
||||
function hasExplicitToolSection(section: unknown): boolean {
|
||||
return section !== undefined && section !== null;
|
||||
}
|
||||
|
||||
function resolveImplicitProfileAlsoAllow(params: {
|
||||
globalTools?: OpenClawConfig["tools"];
|
||||
agentTools?: AgentToolsConfig;
|
||||
}): string[] | undefined {
|
||||
const implicit = new Set<string>();
|
||||
if (
|
||||
hasExplicitToolSection(params.agentTools?.exec) ||
|
||||
hasExplicitToolSection(params.globalTools?.exec)
|
||||
) {
|
||||
implicit.add("exec");
|
||||
implicit.add("process");
|
||||
}
|
||||
if (
|
||||
hasExplicitToolSection(params.agentTools?.fs) ||
|
||||
hasExplicitToolSection(params.globalTools?.fs)
|
||||
) {
|
||||
implicit.add("read");
|
||||
implicit.add("write");
|
||||
implicit.add("edit");
|
||||
}
|
||||
return implicit.size > 0 ? Array.from(implicit) : undefined;
|
||||
}
|
||||
|
||||
export function resolveEffectiveToolPolicy(params: {
|
||||
config?: OpenClawConfig;
|
||||
sessionKey?: string;
|
||||
@@ -226,6 +258,15 @@ export function resolveEffectiveToolPolicy(params: {
|
||||
modelProvider: params.modelProvider,
|
||||
modelId: params.modelId,
|
||||
});
|
||||
const explicitProfileAlsoAllow =
|
||||
resolveExplicitProfileAlsoAllow(agentTools) ?? resolveExplicitProfileAlsoAllow(globalTools);
|
||||
const implicitProfileAlsoAllow = resolveImplicitProfileAlsoAllow({ globalTools, agentTools });
|
||||
const profileAlsoAllow =
|
||||
explicitProfileAlsoAllow || implicitProfileAlsoAllow
|
||||
? Array.from(
|
||||
new Set([...(explicitProfileAlsoAllow ?? []), ...(implicitProfileAlsoAllow ?? [])]),
|
||||
)
|
||||
: undefined;
|
||||
return {
|
||||
agentId,
|
||||
globalPolicy: pickSandboxToolPolicy(globalTools),
|
||||
@@ -235,11 +276,7 @@ export function resolveEffectiveToolPolicy(params: {
|
||||
profile,
|
||||
providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile,
|
||||
// alsoAllow is applied at the profile stage (to avoid being filtered out early).
|
||||
profileAlsoAllow: Array.isArray(agentTools?.alsoAllow)
|
||||
? agentTools?.alsoAllow
|
||||
: Array.isArray(globalTools?.alsoAllow)
|
||||
? globalTools?.alsoAllow
|
||||
: undefined,
|
||||
profileAlsoAllow,
|
||||
providerProfileAlsoAllow: Array.isArray(agentProviderPolicy?.alsoAllow)
|
||||
? agentProviderPolicy?.alsoAllow
|
||||
: Array.isArray(providerPolicy?.alsoAllow)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
resolveEffectiveEnableState,
|
||||
resolveEnableState,
|
||||
} from "./config-state.js";
|
||||
|
||||
describe("normalizePluginsConfig", () => {
|
||||
it("uses default memory slot when not specified", () => {
|
||||
@@ -111,3 +115,34 @@ describe("resolveEffectiveEnableState", () => {
|
||||
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEnableState", () => {
|
||||
it("keeps the selected memory slot plugin enabled even when omitted from plugins.allow", () => {
|
||||
const state = resolveEnableState(
|
||||
"memory-core",
|
||||
"bundled",
|
||||
normalizePluginsConfig({
|
||||
allow: ["telegram"],
|
||||
slots: { memory: "memory-core" },
|
||||
}),
|
||||
);
|
||||
expect(state).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("keeps explicit disable authoritative for the selected memory slot plugin", () => {
|
||||
const state = resolveEnableState(
|
||||
"memory-core",
|
||||
"bundled",
|
||||
normalizePluginsConfig({
|
||||
allow: ["telegram"],
|
||||
slots: { memory: "memory-core" },
|
||||
entries: {
|
||||
"memory-core": {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,19 +197,19 @@ export function resolveEnableState(
|
||||
if (config.deny.includes(id)) {
|
||||
return { enabled: false, reason: "blocked by denylist" };
|
||||
}
|
||||
if (config.allow.length > 0 && !config.allow.includes(id)) {
|
||||
return { enabled: false, reason: "not in allowlist" };
|
||||
const entry = config.entries[id];
|
||||
if (entry?.enabled === false) {
|
||||
return { enabled: false, reason: "disabled in config" };
|
||||
}
|
||||
if (config.slots.memory === id) {
|
||||
return { enabled: true };
|
||||
}
|
||||
const entry = config.entries[id];
|
||||
if (config.allow.length > 0 && !config.allow.includes(id)) {
|
||||
return { enabled: false, reason: "not in allowlist" };
|
||||
}
|
||||
if (entry?.enabled === true) {
|
||||
return { enabled: true };
|
||||
}
|
||||
if (entry?.enabled === false) {
|
||||
return { enabled: false, reason: "disabled in config" };
|
||||
}
|
||||
if (origin === "bundled" && BUNDLED_ENABLED_BY_DEFAULT.has(id)) {
|
||||
return { enabled: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user