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

@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
- Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry.
- Agents/Compaction: strip stale assistant usage snapshots from pre-compaction turns when replaying history after a compaction summary so context-token estimation no longer reuses pre-compaction totals and immediately re-triggers destructive follow-up compactions. (#19127) Thanks @tedwatson.
- Agents/Replies: emit a default completion acknowledgement (`✅ Done.`) when runs execute tools successfully but return no final assistant text, preventing silent no-reply turns after tool-only completions. (#22834) Thanks @Oldshue.
- Agents/Subagents: honor `tools.subagents.tools.alsoAllow` and explicit subagent `allow` entries when resolving built-in subagent deny defaults, so explicitly granted tools (for example `sessions_send`) are no longer blocked unless re-denied in `tools.subagents.tools.deny`. (#23359) Thanks @goren-beehero.
- Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize.
- Plugins/Hooks: run legacy `before_agent_start` once per agent turn and reuse that result across model-resolve and prompt-build compatibility paths, preventing duplicate hook side effects (for example duplicate external API calls). (#23289) Thanks @ksato8710.
- Models/Config: default missing Anthropic provider/model `api` fields to `anthropic-messages` during config validation so custom relay model entries are preserved instead of being dropped by runtime model registry validation. (#23332) Thanks @bigbigmonkey123.

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[];
};
};