mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 22:10:51 +00:00
390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
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";
|
|
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
|
import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js";
|
|
import type { AnyAgentTool } from "./pi-tools.types.js";
|
|
import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js";
|
|
import type { SandboxToolPolicy } from "./sandbox.js";
|
|
import {
|
|
resolveStoredSubagentCapabilities,
|
|
type SubagentSessionRole,
|
|
} from "./subagent-capabilities.js";
|
|
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
|
|
|
function makeToolPolicyMatcher(policy: SandboxToolPolicy) {
|
|
const deny = compileGlobPatterns({
|
|
raw: expandToolGroups(policy.deny ?? []),
|
|
normalize: normalizeToolName,
|
|
});
|
|
const allow = compileGlobPatterns({
|
|
raw: expandToolGroups(policy.allow ?? []),
|
|
normalize: normalizeToolName,
|
|
});
|
|
return (name: string) => {
|
|
const normalized = normalizeToolName(name);
|
|
if (matchesAnyGlobPattern(normalized, deny)) {
|
|
return false;
|
|
}
|
|
if (allow.length === 0) {
|
|
return true;
|
|
}
|
|
if (matchesAnyGlobPattern(normalized, allow)) {
|
|
return true;
|
|
}
|
|
if (normalized === "apply_patch" && matchesAnyGlobPattern("exec", allow)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Tools always denied for sub-agents regardless of depth.
|
|
* These are system-level or interactive tools that sub-agents should never use.
|
|
*/
|
|
const SUBAGENT_TOOL_DENY_ALWAYS = [
|
|
// System admin - dangerous from subagent
|
|
"gateway",
|
|
"agents_list",
|
|
// Interactive setup - not a task
|
|
"whatsapp_login",
|
|
// Status/scheduling - main agent coordinates
|
|
"session_status",
|
|
"cron",
|
|
// Memory - pass relevant info in spawn prompt instead
|
|
"memory_search",
|
|
"memory_get",
|
|
// Direct session sends - subagents communicate through announce chain
|
|
"sessions_send",
|
|
];
|
|
|
|
/**
|
|
* Additional tools denied for leaf sub-agents (depth >= maxSpawnDepth).
|
|
* These are tools that only make sense for orchestrator sub-agents that can spawn children.
|
|
*/
|
|
const SUBAGENT_TOOL_DENY_LEAF = [
|
|
"subagents",
|
|
"sessions_list",
|
|
"sessions_history",
|
|
"sessions_spawn",
|
|
];
|
|
|
|
/**
|
|
* Build the deny list for a sub-agent at a given depth.
|
|
*
|
|
* - Depth 1 with maxSpawnDepth >= 2 (orchestrator): allowed to use sessions_spawn,
|
|
* subagents, sessions_list, sessions_history so it can manage its children.
|
|
* - Depth >= maxSpawnDepth (leaf): denied subagents, sessions_spawn, and
|
|
* session management tools.
|
|
*/
|
|
function resolveSubagentDenyList(depth: number, maxSpawnDepth: number): string[] {
|
|
const isLeaf = depth >= Math.max(1, Math.floor(maxSpawnDepth));
|
|
if (isLeaf) {
|
|
return [...SUBAGENT_TOOL_DENY_ALWAYS, ...SUBAGENT_TOOL_DENY_LEAF];
|
|
}
|
|
// Orchestrator sub-agent: only deny the always-denied tools.
|
|
// sessions_spawn, subagents, sessions_list, sessions_history are allowed.
|
|
return [...SUBAGENT_TOOL_DENY_ALWAYS];
|
|
}
|
|
|
|
function resolveSubagentDenyListForRole(role: SubagentSessionRole): string[] {
|
|
if (role === "leaf") {
|
|
return [...SUBAGENT_TOOL_DENY_ALWAYS, ...SUBAGENT_TOOL_DENY_LEAF];
|
|
}
|
|
return [...SUBAGENT_TOOL_DENY_ALWAYS];
|
|
}
|
|
|
|
export function resolveSubagentToolPolicy(cfg?: OpenClawConfig, depth?: number): SandboxToolPolicy {
|
|
const configured = cfg?.tools?.subagents?.tools;
|
|
const maxSpawnDepth =
|
|
cfg?.agents?.defaults?.subagents?.maxSpawnDepth ?? DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH;
|
|
const effectiveDepth = typeof depth === "number" && depth >= 0 ? depth : 1;
|
|
const baseDeny = resolveSubagentDenyList(effectiveDepth, maxSpawnDepth);
|
|
const allow = Array.isArray(configured?.allow) ? configured.allow : undefined;
|
|
const alsoAllow = Array.isArray(configured?.alsoAllow) ? configured.alsoAllow : undefined;
|
|
const explicitAllow = new Set(
|
|
[...(allow ?? []), ...(alsoAllow ?? [])].map((toolName) => normalizeToolName(toolName)),
|
|
);
|
|
const deny = [
|
|
...baseDeny.filter((toolName) => !explicitAllow.has(normalizeToolName(toolName))),
|
|
...(Array.isArray(configured?.deny) ? configured.deny : []),
|
|
];
|
|
const mergedAllow = allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow;
|
|
return { allow: mergedAllow, deny };
|
|
}
|
|
|
|
export function resolveSubagentToolPolicyForSession(
|
|
cfg: OpenClawConfig | undefined,
|
|
sessionKey: string,
|
|
): SandboxToolPolicy {
|
|
const configured = cfg?.tools?.subagents?.tools;
|
|
const capabilities = resolveStoredSubagentCapabilities(sessionKey, { cfg });
|
|
const allow = Array.isArray(configured?.allow) ? configured.allow : undefined;
|
|
const alsoAllow = Array.isArray(configured?.alsoAllow) ? configured.alsoAllow : undefined;
|
|
const explicitAllow = new Set(
|
|
[...(allow ?? []), ...(alsoAllow ?? [])].map((toolName) => normalizeToolName(toolName)),
|
|
);
|
|
const deny = [
|
|
...resolveSubagentDenyListForRole(capabilities.role).filter(
|
|
(toolName) => !explicitAllow.has(normalizeToolName(toolName)),
|
|
),
|
|
...(Array.isArray(configured?.deny) ? configured.deny : []),
|
|
];
|
|
const mergedAllow = allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow;
|
|
return { allow: mergedAllow, deny };
|
|
}
|
|
|
|
export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean {
|
|
if (!policy) {
|
|
return true;
|
|
}
|
|
return makeToolPolicyMatcher(policy)(name);
|
|
}
|
|
|
|
export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) {
|
|
if (!policy) {
|
|
return tools;
|
|
}
|
|
const matcher = makeToolPolicyMatcher(policy);
|
|
return tools.filter((tool) => matcher(tool.name));
|
|
}
|
|
|
|
type ToolPolicyConfig = {
|
|
allow?: string[];
|
|
alsoAllow?: string[];
|
|
deny?: string[];
|
|
profile?: string;
|
|
};
|
|
|
|
function normalizeProviderKey(value: string): string {
|
|
return value.trim().toLowerCase();
|
|
}
|
|
|
|
function resolveGroupContextFromSessionKey(sessionKey?: string | null): {
|
|
channel?: string;
|
|
groupId?: string;
|
|
} {
|
|
const raw = (sessionKey ?? "").trim();
|
|
if (!raw) {
|
|
return {};
|
|
}
|
|
const base = resolveThreadParentSessionKey(raw) ?? raw;
|
|
const parts = base.split(":").filter(Boolean);
|
|
let body = parts[0] === "agent" ? parts.slice(2) : parts;
|
|
if (body[0] === "subagent") {
|
|
body = body.slice(1);
|
|
}
|
|
if (body.length < 3) {
|
|
return {};
|
|
}
|
|
const [channel, kind, ...rest] = body;
|
|
if (kind !== "group" && kind !== "channel") {
|
|
return {};
|
|
}
|
|
const groupId = rest.join(":").trim();
|
|
if (!groupId) {
|
|
return {};
|
|
}
|
|
return { channel: channel.trim().toLowerCase(), groupId };
|
|
}
|
|
|
|
function resolveProviderToolPolicy(params: {
|
|
byProvider?: Record<string, ToolPolicyConfig>;
|
|
modelProvider?: string;
|
|
modelId?: string;
|
|
}): ToolPolicyConfig | undefined {
|
|
const provider = params.modelProvider?.trim();
|
|
if (!provider || !params.byProvider) {
|
|
return undefined;
|
|
}
|
|
|
|
const entries = Object.entries(params.byProvider);
|
|
if (entries.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const lookup = new Map<string, ToolPolicyConfig>();
|
|
for (const [key, value] of entries) {
|
|
const normalized = normalizeProviderKey(key);
|
|
if (!normalized) {
|
|
continue;
|
|
}
|
|
lookup.set(normalized, value);
|
|
}
|
|
|
|
const normalizedProvider = normalizeProviderKey(provider);
|
|
const rawModelId = params.modelId?.trim().toLowerCase();
|
|
const fullModelId =
|
|
rawModelId && !rawModelId.includes("/") ? `${normalizedProvider}/${rawModelId}` : rawModelId;
|
|
|
|
const candidates = [...(fullModelId ? [fullModelId] : []), normalizedProvider];
|
|
|
|
for (const key of candidates) {
|
|
const match = lookup.get(key);
|
|
if (match) {
|
|
return match;
|
|
}
|
|
}
|
|
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;
|
|
agentId?: string;
|
|
modelProvider?: string;
|
|
modelId?: string;
|
|
}) {
|
|
const explicitAgentId =
|
|
typeof params.agentId === "string" && params.agentId.trim()
|
|
? normalizeAgentId(params.agentId)
|
|
: undefined;
|
|
const agentId =
|
|
explicitAgentId ??
|
|
(params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : undefined);
|
|
const agentConfig =
|
|
params.config && agentId ? resolveAgentConfig(params.config, agentId) : undefined;
|
|
const agentTools = agentConfig?.tools;
|
|
const globalTools = params.config?.tools;
|
|
|
|
const profile = agentTools?.profile ?? globalTools?.profile;
|
|
const providerPolicy = resolveProviderToolPolicy({
|
|
byProvider: globalTools?.byProvider,
|
|
modelProvider: params.modelProvider,
|
|
modelId: params.modelId,
|
|
});
|
|
const agentProviderPolicy = resolveProviderToolPolicy({
|
|
byProvider: agentTools?.byProvider,
|
|
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),
|
|
globalProviderPolicy: pickSandboxToolPolicy(providerPolicy),
|
|
agentPolicy: pickSandboxToolPolicy(agentTools),
|
|
agentProviderPolicy: pickSandboxToolPolicy(agentProviderPolicy),
|
|
profile,
|
|
providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile,
|
|
// alsoAllow is applied at the profile stage (to avoid being filtered out early).
|
|
profileAlsoAllow,
|
|
providerProfileAlsoAllow: Array.isArray(agentProviderPolicy?.alsoAllow)
|
|
? agentProviderPolicy?.alsoAllow
|
|
: Array.isArray(providerPolicy?.alsoAllow)
|
|
? providerPolicy?.alsoAllow
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
export function resolveGroupToolPolicy(params: {
|
|
config?: OpenClawConfig;
|
|
sessionKey?: string;
|
|
spawnedBy?: string | null;
|
|
messageProvider?: string;
|
|
groupId?: string | null;
|
|
groupChannel?: string | null;
|
|
groupSpace?: string | null;
|
|
accountId?: string | null;
|
|
senderId?: string | null;
|
|
senderName?: string | null;
|
|
senderUsername?: string | null;
|
|
senderE164?: string | null;
|
|
}): SandboxToolPolicy | undefined {
|
|
if (!params.config) {
|
|
return undefined;
|
|
}
|
|
const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey);
|
|
const spawnedContext = resolveGroupContextFromSessionKey(params.spawnedBy);
|
|
const groupId = params.groupId ?? sessionContext.groupId ?? spawnedContext.groupId;
|
|
if (!groupId) {
|
|
return undefined;
|
|
}
|
|
const channelRaw = params.messageProvider ?? sessionContext.channel ?? spawnedContext.channel;
|
|
const channel = normalizeMessageChannel(channelRaw);
|
|
if (!channel) {
|
|
return undefined;
|
|
}
|
|
let dock;
|
|
try {
|
|
dock = getChannelDock(channel);
|
|
} catch {
|
|
dock = undefined;
|
|
}
|
|
const toolsConfig =
|
|
dock?.groups?.resolveToolPolicy?.({
|
|
cfg: params.config,
|
|
groupId,
|
|
groupChannel: params.groupChannel,
|
|
groupSpace: params.groupSpace,
|
|
accountId: params.accountId,
|
|
senderId: params.senderId,
|
|
senderName: params.senderName,
|
|
senderUsername: params.senderUsername,
|
|
senderE164: params.senderE164,
|
|
}) ??
|
|
resolveChannelGroupToolsPolicy({
|
|
cfg: params.config,
|
|
channel,
|
|
groupId,
|
|
accountId: params.accountId,
|
|
senderId: params.senderId,
|
|
senderName: params.senderName,
|
|
senderUsername: params.senderUsername,
|
|
senderE164: params.senderE164,
|
|
});
|
|
return pickSandboxToolPolicy(toolsConfig);
|
|
}
|
|
|
|
export function isToolAllowedByPolicies(
|
|
name: string,
|
|
policies: Array<SandboxToolPolicy | undefined>,
|
|
) {
|
|
return policies.every((policy) => isToolAllowedByPolicyName(name, policy));
|
|
}
|