Agents/Subagents: honor subagent alsoAllow grants

This commit is contained in:
Vignesh Natarajan
2026-02-22 00:39:16 -08:00
parent 2d2e1c2403
commit 2a66c8d676
4 changed files with 70 additions and 2 deletions

View File

@@ -54,6 +54,63 @@ describe("resolveSubagentToolPolicy depth awareness", () => {
agents: { defaults: { subagents: { maxSpawnDepth: 1 } } },
} as unknown as OpenClawConfig;
it("applies subagent tools.alsoAllow to re-enable default-denied tools", () => {
const cfg = {
agents: { defaults: { subagents: { maxSpawnDepth: 2 } } },
tools: { subagents: { tools: { alsoAllow: ["sessions_send"] } } },
} as unknown as OpenClawConfig;
const policy = resolveSubagentToolPolicy(cfg, 1);
expect(isToolAllowedByPolicyName("sessions_send", policy)).toBe(true);
expect(isToolAllowedByPolicyName("cron", policy)).toBe(false);
});
it("applies subagent tools.allow to re-enable default-denied tools", () => {
const cfg = {
agents: { defaults: { subagents: { maxSpawnDepth: 2 } } },
tools: { subagents: { tools: { allow: ["sessions_send"] } } },
} as unknown as OpenClawConfig;
const policy = resolveSubagentToolPolicy(cfg, 1);
expect(isToolAllowedByPolicyName("sessions_send", policy)).toBe(true);
});
it("merges subagent tools.alsoAllow into tools.allow when both are set", () => {
const cfg = {
agents: { defaults: { subagents: { maxSpawnDepth: 2 } } },
tools: {
subagents: { tools: { allow: ["sessions_spawn"], alsoAllow: ["sessions_send"] } },
},
} as unknown as OpenClawConfig;
const policy = resolveSubagentToolPolicy(cfg, 1);
expect(policy.allow).toEqual(["sessions_spawn", "sessions_send"]);
});
it("keeps configured deny precedence over allow and alsoAllow", () => {
const cfg = {
agents: { defaults: { subagents: { maxSpawnDepth: 2 } } },
tools: {
subagents: {
tools: {
allow: ["sessions_send"],
alsoAllow: ["sessions_send"],
deny: ["sessions_send"],
},
},
},
} as unknown as OpenClawConfig;
const policy = resolveSubagentToolPolicy(cfg, 1);
expect(isToolAllowedByPolicyName("sessions_send", policy)).toBe(false);
});
it("does not create a restrictive allowlist when only alsoAllow is configured", () => {
const cfg = {
agents: { defaults: { subagents: { maxSpawnDepth: 2 } } },
tools: { subagents: { tools: { alsoAllow: ["sessions_send"] } } },
} as unknown as OpenClawConfig;
const policy = resolveSubagentToolPolicy(cfg, 1);
expect(policy.allow).toBeUndefined();
expect(isToolAllowedByPolicyName("subagents", policy)).toBe(true);
});
it("depth-1 orchestrator (maxSpawnDepth=2) allows sessions_spawn", () => {
const policy = resolveSubagentToolPolicy(baseCfg, 1);
expect(isToolAllowedByPolicyName("sessions_spawn", policy)).toBe(true);

View File

@@ -88,9 +88,17 @@ export function resolveSubagentToolPolicy(cfg?: OpenClawConfig, depth?: number):
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 deny = [...baseDeny, ...(Array.isArray(configured?.deny) ? configured.deny : [])];
const allow = Array.isArray(configured?.allow) ? configured.allow : undefined;
return { allow, deny };
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 isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean {

View File

@@ -520,6 +520,8 @@ export type ToolsConfig = {
model?: string | { primary?: string; fallbacks?: string[] };
tools?: {
allow?: string[];
/** Additional allowlist entries merged into allow and/or default sub-agent denylist. */
alsoAllow?: string[];
deny?: string[];
};
};