mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 10:59:39 +00:00
Inherit tool restrictions for delegated sessions [AI] (#80979)
* fix: inherit tool restrictions for delegated sessions * addressing review-skill * addressing review-skill * addressing review-skill * addressing review-skill * addressing codex review * addressing codex review * addressing codex review * addressing codex review * addressing codex review * addressing review-skill * addressing codex review * addressing claude review * addressing ci * docs: add changelog entry for PR merge
This commit is contained in:
parent
aba7652b76
commit
6c918ca85f
27 changed files with 992 additions and 20 deletions
|
|
@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
### Fixes
|
||||
|
||||
- Inherit tool restrictions for delegated sessions [AI]. (#80979) Thanks @pgondhi987.
|
||||
- Telegram: discard legacy long-poll update offsets that cannot be tied to the current bot token, so token rotation no longer leaves bots silently skipping new messages. (#80671) Thanks @sxxtony.
|
||||
- browser: enforce navigation checks for act interactions [AI]. (#81070) Thanks @pgondhi987.
|
||||
- Validate node exec event provenance [AI]. (#81071) Thanks @pgondhi987.
|
||||
|
|
|
|||
|
|
@ -2098,6 +2098,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
public let spawndepth: AnyCodable?
|
||||
public let subagentrole: AnyCodable?
|
||||
public let subagentcontrolscope: AnyCodable?
|
||||
public let inheritedtoolallow: AnyCodable?
|
||||
public let inheritedtooldeny: AnyCodable?
|
||||
public let sendpolicy: AnyCodable?
|
||||
public let groupactivation: AnyCodable?
|
||||
|
||||
|
|
@ -2121,6 +2123,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
spawndepth: AnyCodable?,
|
||||
subagentrole: AnyCodable?,
|
||||
subagentcontrolscope: AnyCodable?,
|
||||
inheritedtoolallow: AnyCodable?,
|
||||
inheritedtooldeny: AnyCodable?,
|
||||
sendpolicy: AnyCodable?,
|
||||
groupactivation: AnyCodable?)
|
||||
{
|
||||
|
|
@ -2143,6 +2147,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
self.spawndepth = spawndepth
|
||||
self.subagentrole = subagentrole
|
||||
self.subagentcontrolscope = subagentcontrolscope
|
||||
self.inheritedtoolallow = inheritedtoolallow
|
||||
self.inheritedtooldeny = inheritedtooldeny
|
||||
self.sendpolicy = sendpolicy
|
||||
self.groupactivation = groupactivation
|
||||
}
|
||||
|
|
@ -2167,6 +2173,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
case spawndepth = "spawnDepth"
|
||||
case subagentrole = "subagentRole"
|
||||
case subagentcontrolscope = "subagentControlScope"
|
||||
case inheritedtoolallow = "inheritedToolAllow"
|
||||
case inheritedtooldeny = "inheritedToolDeny"
|
||||
case sendpolicy = "sendPolicy"
|
||||
case groupactivation = "groupActivation"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,14 @@ import {
|
|||
startAcpSpawnParentStreamRelay,
|
||||
} from "./acp-spawn-parent-stream.js";
|
||||
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope.js";
|
||||
import {
|
||||
findAcpUnsupportedInheritedToolAllow,
|
||||
findAcpUnsupportedInheritedToolDeny,
|
||||
formatAcpInheritedToolAllowError,
|
||||
formatAcpInheritedToolDenyError,
|
||||
inheritedToolAllowPatch,
|
||||
inheritedToolDenyPatch,
|
||||
} from "./inherited-tool-deny.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
||||
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
|
||||
import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js";
|
||||
|
|
@ -123,6 +131,8 @@ export type SpawnAcpContext = {
|
|||
/** Trusted provider role ids for the requester in this group turn. */
|
||||
agentMemberRoleIds?: string[];
|
||||
sandboxed?: boolean;
|
||||
inheritedToolAllowlist?: string[];
|
||||
inheritedToolDenylist?: string[];
|
||||
};
|
||||
|
||||
export const ACP_SPAWN_ERROR_CODES = [
|
||||
|
|
@ -1157,6 +1167,26 @@ export async function spawnAcpDirect(
|
|||
error: runtimePolicyError,
|
||||
});
|
||||
}
|
||||
const acpUnsupportedInheritedTool = findAcpUnsupportedInheritedToolDeny(
|
||||
ctx.inheritedToolDenylist,
|
||||
);
|
||||
if (acpUnsupportedInheritedTool) {
|
||||
return createAcpSpawnFailure({
|
||||
status: "forbidden",
|
||||
errorCode: "runtime_policy",
|
||||
error: formatAcpInheritedToolDenyError(acpUnsupportedInheritedTool),
|
||||
});
|
||||
}
|
||||
const acpUnsupportedInheritedAllow = findAcpUnsupportedInheritedToolAllow(
|
||||
ctx.inheritedToolAllowlist,
|
||||
);
|
||||
if (acpUnsupportedInheritedAllow) {
|
||||
return createAcpSpawnFailure({
|
||||
status: "forbidden",
|
||||
errorCode: "runtime_policy",
|
||||
error: formatAcpInheritedToolAllowError(acpUnsupportedInheritedAllow),
|
||||
});
|
||||
}
|
||||
|
||||
const spawnMode = resolveSpawnMode({
|
||||
requestedMode: params.mode,
|
||||
|
|
@ -1291,6 +1321,8 @@ export async function spawnAcpDirect(
|
|||
key: sessionKey,
|
||||
spawnedBy: requesterInternalKey,
|
||||
...subagentEnvelopeState.childSessionPatch,
|
||||
...inheritedToolAllowPatch(ctx.inheritedToolAllowlist),
|
||||
...inheritedToolDenyPatch(ctx.inheritedToolDenylist),
|
||||
...(params.label ? { label: params.label } : {}),
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
|
|
|
|||
90
src/agents/inherited-tool-deny.ts
Normal file
90
src/agents/inherited-tool-deny.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { isToolAllowedByPolicyName } from "./tool-policy-match.js";
|
||||
import { normalizeToolName } from "./tool-policy-shared.js";
|
||||
|
||||
const ACP_UNSUPPORTED_INHERITED_TOOL_DENY = [
|
||||
"apply_patch",
|
||||
"edit",
|
||||
"exec",
|
||||
"fs_delete",
|
||||
"fs_move",
|
||||
"fs_write",
|
||||
"process",
|
||||
"read",
|
||||
"shell",
|
||||
"spawn",
|
||||
"write",
|
||||
] as const;
|
||||
|
||||
// Inherited allowlists are rebuilt from the effective OpenClaw tool surface.
|
||||
// ACP-only aliases can appear in explicit deny policies, but not in that
|
||||
// effective allowlist unless a plugin happens to expose matching tool names.
|
||||
const ACP_REQUIRED_INHERITED_TOOL_ALLOW = [
|
||||
"apply_patch",
|
||||
"edit",
|
||||
"exec",
|
||||
"process",
|
||||
"read",
|
||||
"write",
|
||||
] as const;
|
||||
|
||||
export function normalizeInheritedToolDenylist(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const entry of value) {
|
||||
if (typeof entry !== "string") {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeToolName(entry);
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function inheritedToolDenyPatch(value: unknown): { inheritedToolDeny?: string[] } {
|
||||
const inheritedToolDeny = normalizeInheritedToolDenylist(value);
|
||||
return inheritedToolDeny.length > 0 ? { inheritedToolDeny } : {};
|
||||
}
|
||||
|
||||
export function normalizeInheritedToolAllowlist(value: unknown): string[] {
|
||||
return normalizeInheritedToolDenylist(value);
|
||||
}
|
||||
|
||||
export function inheritedToolAllowPatch(value: unknown): { inheritedToolAllow?: string[] } {
|
||||
const inheritedToolAllow = normalizeInheritedToolAllowlist(value);
|
||||
return inheritedToolAllow.length > 0 ? { inheritedToolAllow } : {};
|
||||
}
|
||||
|
||||
export function findAcpUnsupportedInheritedToolDeny(value: unknown): string | undefined {
|
||||
const inheritedToolDeny = normalizeInheritedToolDenylist(value);
|
||||
if (inheritedToolDeny.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return ACP_UNSUPPORTED_INHERITED_TOOL_DENY.find(
|
||||
(toolName) => !isToolAllowedByPolicyName(toolName, { deny: inheritedToolDeny }),
|
||||
);
|
||||
}
|
||||
|
||||
export function findAcpUnsupportedInheritedToolAllow(value: unknown): string | undefined {
|
||||
const inheritedToolAllow = normalizeInheritedToolAllowlist(value);
|
||||
if (inheritedToolAllow.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return ACP_REQUIRED_INHERITED_TOOL_ALLOW.find(
|
||||
(toolName) => !isToolAllowedByPolicyName(toolName, { allow: inheritedToolAllow }),
|
||||
);
|
||||
}
|
||||
|
||||
export function formatAcpInheritedToolDenyError(toolName: string): string {
|
||||
return `runtime="acp" is unavailable because the requester denies ${toolName}. Use runtime="subagent".`;
|
||||
}
|
||||
|
||||
export function formatAcpInheritedToolAllowError(toolName: string): string {
|
||||
return `runtime="acp" is unavailable because the requester does not allow ${toolName}. Use runtime="subagent".`;
|
||||
}
|
||||
|
|
@ -414,6 +414,8 @@ export function createOpenClawTools(
|
|||
config: resolvedConfig,
|
||||
requesterAgentIdOverride: options?.requesterAgentIdOverride,
|
||||
workspaceDir: spawnWorkspaceDir,
|
||||
inheritedToolAllowlist: options?.inheritedToolAllowlist,
|
||||
inheritedToolDenylist: options?.inheritedToolDenylist,
|
||||
}),
|
||||
]),
|
||||
createSessionsYieldTool({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { setPluginToolMeta } from "../../plugins/tools.js";
|
||||
import { providerAliasCases } from "../test-helpers/provider-alias-cases.js";
|
||||
|
|
@ -46,6 +49,91 @@ describe("applyFinalEffectiveToolPolicy", () => {
|
|||
expect(filtered.map((tool) => tool.name)).toEqual(["mcp__bundle__fs_read"]);
|
||||
});
|
||||
|
||||
it("filters bundled tools through inherited subagent allowlists", () => {
|
||||
const agentId = `bundled-inherited-allow-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const sessionKey = `agent:${agentId}:subagent:limited`;
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-bundled-inherited-allow-${agentId}.json`);
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[sessionKey]: {
|
||||
sessionId: "limited-session",
|
||||
updatedAt: Date.now(),
|
||||
spawnDepth: 1,
|
||||
subagentRole: "orchestrator",
|
||||
subagentControlScope: "children",
|
||||
inheritedToolAllow: ["mcp__bundle__fs_read"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const filtered = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [makeTool("mcp__bundle__fs_delete"), makeTool("mcp__bundle__fs_read")],
|
||||
config: {
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
},
|
||||
sessionKey,
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
expect(filtered.map((tool) => tool.name)).toEqual(["mcp__bundle__fs_read"]);
|
||||
});
|
||||
|
||||
it("honors configured plugin allow entries alongside inherited bundled tool allows", () => {
|
||||
const agentId = `bundled-plugin-allow-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const sessionKey = `agent:${agentId}:subagent:limited`;
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-bundled-plugin-allow-${agentId}.json`);
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[sessionKey]: {
|
||||
sessionId: "limited-session",
|
||||
updatedAt: Date.now(),
|
||||
spawnDepth: 1,
|
||||
subagentRole: "orchestrator",
|
||||
subagentControlScope: "children",
|
||||
inheritedToolAllow: ["mcp__bundle__fs_read"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const deniedTool = makeTool("mcp__bundle__fs_delete");
|
||||
const allowedTool = makeTool("mcp__bundle__fs_read");
|
||||
setPluginToolMeta(deniedTool, { pluginId: "bundle-mcp", optional: false });
|
||||
setPluginToolMeta(allowedTool, { pluginId: "bundle-mcp", optional: false });
|
||||
|
||||
const filtered = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [deniedTool, allowedTool],
|
||||
config: {
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
tools: {
|
||||
subagents: {
|
||||
tools: {
|
||||
allow: ["bundle-mcp"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sessionKey,
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
expect(filtered.map((tool) => tool.name)).toEqual(["mcp__bundle__fs_read"]);
|
||||
});
|
||||
|
||||
it("applies channel-normalized per-sender policy to bundled tools", () => {
|
||||
const filtered = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [makeTool("mcp__bundle__exec"), makeTool("mcp__bundle__read")],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { getPluginToolMeta } from "../../plugins/tools.js";
|
|||
import {
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveInheritedToolPolicyForSession,
|
||||
resolveTrustedGroupId,
|
||||
resolveSubagentToolPolicyForSession,
|
||||
} from "../pi-tools.policy.js";
|
||||
|
|
@ -136,6 +137,13 @@ export function applyFinalEffectiveToolPolicy(
|
|||
store: subagentStore,
|
||||
})
|
||||
: undefined;
|
||||
const inheritedToolPolicy = resolveInheritedToolPolicyForSession(
|
||||
params.config,
|
||||
params.sessionKey,
|
||||
{
|
||||
store: subagentStore,
|
||||
},
|
||||
);
|
||||
const ownerFiltered = applyOwnerOnlyToolPolicy(
|
||||
params.bundledTools,
|
||||
params.senderIsOwner === true,
|
||||
|
|
@ -169,6 +177,7 @@ export function applyFinalEffectiveToolPolicy(
|
|||
}),
|
||||
{ policy: params.sandboxToolPolicy, label: "sandbox tools.allow" },
|
||||
{ policy: subagentPolicy, label: "subagent tools.allow" },
|
||||
{ policy: inheritedToolPolicy, label: "inherited tools" },
|
||||
].map((step) => Object.assign({}, step, { suppressUnavailableCoreToolWarning: true }));
|
||||
return applyToolPolicyPipeline({
|
||||
tools: ownerFiltered,
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ import {
|
|||
import {
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveInheritedToolPolicyForSession,
|
||||
resolveSubagentToolPolicyForSession,
|
||||
} from "../../pi-tools.policy.js";
|
||||
import { wrapStreamFnTextTransforms } from "../../plugin-text-transforms.js";
|
||||
|
|
@ -763,6 +764,13 @@ function collectAttemptExplicitToolAllowlistSources(params: {
|
|||
store: subagentStore,
|
||||
})
|
||||
: undefined;
|
||||
const inheritedToolPolicy = resolveInheritedToolPolicyForSession(
|
||||
params.config,
|
||||
params.sandboxSessionKey,
|
||||
{
|
||||
store: subagentStore,
|
||||
},
|
||||
);
|
||||
return collectExplicitToolAllowlistSources([
|
||||
{ label: "tools.allow", allow: globalPolicy?.allow },
|
||||
{ label: "tools.byProvider.allow", allow: globalProviderPolicy?.allow },
|
||||
|
|
@ -777,6 +785,7 @@ function collectAttemptExplicitToolAllowlistSources(params: {
|
|||
{ label: "group tools.allow", allow: groupPolicy?.allow },
|
||||
{ label: "sandbox tools.allow", allow: params.sandboxToolPolicy?.allow },
|
||||
{ label: "subagent tools.allow", allow: subagentPolicy?.allow },
|
||||
{ label: "inherited tools.allow", allow: inheritedToolPolicy?.allow },
|
||||
{ label: "runtime toolsAllow", allow: params.toolsAllow, enforceWhenToolsDisabled: true },
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -485,6 +485,56 @@ describe("createOpenClawCodingTools", () => {
|
|||
expectListIncludes(latestCreateOpenClawToolsOptions().pluginToolDenylist, ["pdf"]);
|
||||
});
|
||||
|
||||
it("passes inherited allowlist entries to OpenClaw plugin discovery", async () => {
|
||||
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
|
||||
createOpenClawToolsMock.mockClear();
|
||||
const agentId = `inherited-allow-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const storeTemplate = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-session-store-${agentId}-{agentId}.json`,
|
||||
);
|
||||
await writeSessionStore(storeTemplate, agentId, {
|
||||
[`agent:${agentId}:subagent:limited`]: {
|
||||
sessionId: "limited-session",
|
||||
updatedAt: Date.now(),
|
||||
spawnDepth: 1,
|
||||
subagentRole: "orchestrator",
|
||||
subagentControlScope: "children",
|
||||
inheritedToolAllow: ["custom_plugin_tool", "sessions_spawn"],
|
||||
},
|
||||
});
|
||||
|
||||
createOpenClawCodingTools({
|
||||
sessionKey: `agent:${agentId}:subagent:limited`,
|
||||
config: {
|
||||
session: {
|
||||
store: storeTemplate,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
|
||||
expectListIncludes(latestCreateOpenClawToolsOptions().pluginToolAllowlist, [
|
||||
"custom_plugin_tool",
|
||||
"sessions_spawn",
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes effective allow-list-restricted tool surface to spawned sessions", () => {
|
||||
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
|
||||
createOpenClawToolsMock.mockClear();
|
||||
|
||||
createOpenClawCodingTools({
|
||||
config: { tools: { allow: ["read", "sessions_spawn"] } },
|
||||
});
|
||||
|
||||
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
|
||||
const inheritedAllow = latestCreateOpenClawToolsOptions().inheritedToolAllowlist;
|
||||
expectListIncludes(inheritedAllow, ["read", "sessions_spawn"]);
|
||||
expect(inheritedAllow?.includes("exec")).toBe(false);
|
||||
expect(inheritedAllow?.includes("process")).toBe(false);
|
||||
});
|
||||
|
||||
it("records core tool-prep stages for hot-path diagnostics", () => {
|
||||
const stages: string[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
isToolAllowedByPolicyName,
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveInheritedToolPolicyForSession,
|
||||
resolveSubagentToolPolicy,
|
||||
resolveSubagentToolPolicyForSession,
|
||||
resolveTrustedGroupId,
|
||||
|
|
@ -370,6 +371,165 @@ describe("resolveSubagentToolPolicy depth awareness", () => {
|
|||
expect(isToolAllowedByPolicyName("memory_get", policy)).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves inherited tool denies from stored subagent sessions", () => {
|
||||
const storePath = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-subagent-inherited-deny-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:subagent:limited": {
|
||||
sessionId: "limited-session",
|
||||
updatedAt: Date.now(),
|
||||
spawnDepth: 1,
|
||||
subagentRole: "orchestrator",
|
||||
subagentControlScope: "children",
|
||||
inheritedToolDeny: ["bash", "memory_get"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const policy = resolveInheritedToolPolicyForSession(cfg, "agent:main:subagent:limited");
|
||||
expect(isToolAllowedByPolicyName("exec", policy)).toBe(false);
|
||||
expect(isToolAllowedByPolicyName("memory_get", policy)).toBe(false);
|
||||
expect(isToolAllowedByPolicyName("sessions_spawn", policy)).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves inherited tool allows from stored subagent sessions", () => {
|
||||
const storePath = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-subagent-inherited-allow-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:subagent:limited": {
|
||||
sessionId: "limited-session",
|
||||
updatedAt: Date.now(),
|
||||
spawnDepth: 1,
|
||||
subagentRole: "orchestrator",
|
||||
subagentControlScope: "children",
|
||||
inheritedToolAllow: ["sessions_spawn", "memory_search"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const policy = resolveInheritedToolPolicyForSession(cfg, "agent:main:subagent:limited");
|
||||
expect(isToolAllowedByPolicyName("sessions_spawn", policy)).toBe(true);
|
||||
expect(isToolAllowedByPolicyName("memory_search", policy)).toBe(true);
|
||||
expect(isToolAllowedByPolicyName("read", policy)).toBe(false);
|
||||
expect(isToolAllowedByPolicyName("exec", policy)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps configured plugin allows separate from inherited tool allows", () => {
|
||||
const storePath = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-subagent-inherited-allow-separate-${Date.now()}-${Math.random()
|
||||
.toString(16)
|
||||
.slice(2)}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:subagent:limited": {
|
||||
sessionId: "limited-session",
|
||||
updatedAt: Date.now(),
|
||||
spawnDepth: 1,
|
||||
subagentRole: "orchestrator",
|
||||
subagentControlScope: "children",
|
||||
inheritedToolAllow: ["plugin_tool"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
tools: {
|
||||
subagents: {
|
||||
tools: {
|
||||
allow: ["plugin-id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const subagentPolicy = resolveSubagentToolPolicyForSession(cfg, "agent:main:subagent:limited");
|
||||
const inheritedPolicy = resolveInheritedToolPolicyForSession(
|
||||
cfg,
|
||||
"agent:main:subagent:limited",
|
||||
);
|
||||
expect(subagentPolicy.allow).toEqual(["plugin-id"]);
|
||||
expect(inheritedPolicy?.allow).toEqual(["plugin_tool"]);
|
||||
});
|
||||
|
||||
it("applies inherited tool policy from stored ACP sessions without subagent metadata", () => {
|
||||
const storePath = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-acp-inherited-deny-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:acp:limited": {
|
||||
sessionId: "limited-acp-session",
|
||||
updatedAt: Date.now(),
|
||||
inheritedToolAllow: ["custom_plugin_tool"],
|
||||
inheritedToolDeny: ["custom_denied_tool"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const policy = resolveInheritedToolPolicyForSession(cfg, "agent:main:acp:limited");
|
||||
expect(isToolAllowedByPolicyName("custom_plugin_tool", policy)).toBe(true);
|
||||
expect(isToolAllowedByPolicyName("custom_denied_tool", policy)).toBe(false);
|
||||
expect(isToolAllowedByPolicyName("read", policy)).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to leaf behavior when no depth is provided", () => {
|
||||
const policy = resolveSubagentToolPolicy(baseCfg);
|
||||
// Default depth=1, maxSpawnDepth=2 → orchestrator
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js";
|
|||
import type { SandboxToolPolicy } from "./sandbox.js";
|
||||
import {
|
||||
resolveSubagentCapabilityStore,
|
||||
resolveStoredSubagentInheritedToolAllowlist,
|
||||
resolveStoredSubagentInheritedToolDenylist,
|
||||
resolveStoredSubagentCapabilities,
|
||||
type SessionCapabilityStore,
|
||||
type SubagentSessionRole,
|
||||
|
|
@ -84,6 +86,13 @@ function resolveSubagentDenyListForRole(role: SubagentSessionRole): string[] {
|
|||
return [...SUBAGENT_TOOL_DENY_ALWAYS];
|
||||
}
|
||||
|
||||
function mergeConfiguredSubagentAllow(
|
||||
allow: string[] | undefined,
|
||||
alsoAllow: string[] | undefined,
|
||||
): string[] | undefined {
|
||||
return allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow;
|
||||
}
|
||||
|
||||
export function resolveSubagentToolPolicy(cfg?: OpenClawConfig, depth?: number): SandboxToolPolicy {
|
||||
const configured = cfg?.tools?.subagents?.tools;
|
||||
const maxSpawnDepth =
|
||||
|
|
@ -99,7 +108,7 @@ export function resolveSubagentToolPolicy(cfg?: OpenClawConfig, depth?: number):
|
|||
...baseDeny.filter((toolName) => !explicitAllow.has(normalizeToolName(toolName))),
|
||||
...(Array.isArray(configured?.deny) ? configured.deny : []),
|
||||
];
|
||||
const mergedAllow = allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow;
|
||||
const mergedAllow = mergeConfiguredSubagentAllow(allow, alsoAllow);
|
||||
return { allow: mergedAllow, deny };
|
||||
}
|
||||
|
||||
|
|
@ -130,10 +139,34 @@ export function resolveSubagentToolPolicyForSession(
|
|||
),
|
||||
...(Array.isArray(configured?.deny) ? configured.deny : []),
|
||||
];
|
||||
const mergedAllow = allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow;
|
||||
const mergedAllow = mergeConfiguredSubagentAllow(allow, alsoAllow);
|
||||
return { allow: mergedAllow, deny };
|
||||
}
|
||||
|
||||
export function resolveInheritedToolPolicyForSession(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
sessionKey: string | undefined | null,
|
||||
opts?: {
|
||||
store?: SessionCapabilityStore;
|
||||
},
|
||||
): SandboxToolPolicy | undefined {
|
||||
const inheritedToolAllow = resolveStoredSubagentInheritedToolAllowlist(sessionKey, {
|
||||
cfg,
|
||||
store: opts?.store,
|
||||
});
|
||||
const inheritedToolDeny = resolveStoredSubagentInheritedToolDenylist(sessionKey, {
|
||||
cfg,
|
||||
store: opts?.store,
|
||||
});
|
||||
if (inheritedToolAllow.length === 0 && inheritedToolDeny.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(inheritedToolAllow.length > 0 ? { allow: inheritedToolAllow } : {}),
|
||||
...(inheritedToolDeny.length > 0 ? { deny: inheritedToolDeny } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) {
|
||||
if (!policy) {
|
||||
return tools;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
isToolAllowedByPolicies,
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveInheritedToolPolicyForSession,
|
||||
resolveSubagentToolPolicyForSession,
|
||||
} from "./pi-tools.policy.js";
|
||||
import {
|
||||
|
|
@ -77,8 +78,10 @@ import {
|
|||
applyOwnerOnlyToolPolicy,
|
||||
collectExplicitAllowlist,
|
||||
collectExplicitDenylist,
|
||||
hasRestrictiveAllowPolicy,
|
||||
mergeAlsoAllowPolicy,
|
||||
normalizeToolName,
|
||||
replaceWithEffectiveToolAllowlist,
|
||||
resolveToolProfilePolicy,
|
||||
} from "./tool-policy.js";
|
||||
import {
|
||||
|
|
@ -553,6 +556,13 @@ export function createOpenClawCodingTools(options?: {
|
|||
store: subagentStore,
|
||||
})
|
||||
: undefined;
|
||||
const inheritedToolPolicy = resolveInheritedToolPolicyForSession(
|
||||
options?.config,
|
||||
options?.sessionKey,
|
||||
{
|
||||
store: subagentStore,
|
||||
},
|
||||
);
|
||||
const globalPolicyWithToolSearchControls = mergeToolSearchControlAllowlist(globalPolicy);
|
||||
const globalProviderPolicyWithToolSearchControls =
|
||||
mergeToolSearchControlAllowlist(globalProviderPolicy);
|
||||
|
|
@ -575,6 +585,7 @@ export function createOpenClawCodingTools(options?: {
|
|||
senderPolicyWithToolSearchControls,
|
||||
sandboxToolPolicyWithToolSearchControls,
|
||||
subagentPolicyWithToolSearchControls,
|
||||
inheritedToolPolicy,
|
||||
]);
|
||||
options?.recordToolPrepStage?.("tool-policy");
|
||||
const execConfig = resolveExecConfig({ cfg: options?.config, agentId });
|
||||
|
|
@ -745,6 +756,7 @@ export function createOpenClawCodingTools(options?: {
|
|||
senderPolicy,
|
||||
sandboxToolPolicy,
|
||||
subagentPolicy,
|
||||
inheritedToolPolicy,
|
||||
options?.runtimeToolAllowlist ? { allow: options.runtimeToolAllowlist } : undefined,
|
||||
]);
|
||||
const pluginToolDenylist = collectExplicitDenylist([
|
||||
|
|
@ -758,7 +770,26 @@ export function createOpenClawCodingTools(options?: {
|
|||
senderPolicy,
|
||||
sandboxToolPolicy,
|
||||
subagentPolicy,
|
||||
inheritedToolPolicy,
|
||||
]);
|
||||
const inheritedToolDenylist = [...pluginToolDenylist];
|
||||
// Passed by reference to sessions_spawn and populated after the final policy
|
||||
// pass so child sessions inherit the actual parent tool surface.
|
||||
const inheritedToolAllowlist: string[] = [];
|
||||
const shouldInheritEffectiveToolAllowlist = [
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
senderPolicy,
|
||||
sandboxToolPolicy,
|
||||
subagentPolicy,
|
||||
inheritedToolPolicy,
|
||||
options?.runtimeToolAllowlist ? { allow: options.runtimeToolAllowlist } : undefined,
|
||||
].some(hasRestrictiveAllowPolicy);
|
||||
const pluginToolsOnly =
|
||||
includeOpenClawTools || !includePluginTools
|
||||
? []
|
||||
|
|
@ -885,6 +916,8 @@ export function createOpenClawCodingTools(options?: {
|
|||
authProfileStore: options?.authProfileStore,
|
||||
senderIsOwner: options?.senderIsOwner,
|
||||
sessionId: options?.sessionId,
|
||||
inheritedToolAllowlist,
|
||||
inheritedToolDenylist,
|
||||
onYield: options?.onYield,
|
||||
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
|
||||
recordToolPrepStage: options?.recordToolPrepStage,
|
||||
|
|
@ -960,8 +993,12 @@ export function createOpenClawCodingTools(options?: {
|
|||
}),
|
||||
{ policy: sandboxToolPolicyWithToolSearchControls, label: "sandbox tools.allow" },
|
||||
{ policy: subagentPolicyWithToolSearchControls, label: "subagent tools.allow" },
|
||||
{ policy: inheritedToolPolicy, label: "inherited tools" },
|
||||
],
|
||||
});
|
||||
if (shouldInheritEffectiveToolAllowlist) {
|
||||
replaceWithEffectiveToolAllowlist(inheritedToolAllowlist, subagentFiltered);
|
||||
}
|
||||
options?.recordToolPrepStage?.("authorization-policy");
|
||||
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
||||
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export type SpawnedToolContext = {
|
|||
agentGroupSpace?: string | null;
|
||||
agentMemberRoleIds?: string[];
|
||||
workspaceDir?: string;
|
||||
inheritedToolAllowlist?: string[];
|
||||
inheritedToolDenylist?: string[];
|
||||
};
|
||||
|
||||
type NormalizedSpawnedRunMetadata = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import {
|
|||
parseAgentSessionKey,
|
||||
} from "../routing/session-key.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeInheritedToolAllowlist,
|
||||
normalizeInheritedToolDenylist,
|
||||
} from "./inherited-tool-deny.js";
|
||||
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||
import { normalizeSubagentSessionKey } from "./subagent-session-key.js";
|
||||
|
||||
|
|
@ -26,6 +30,8 @@ type SessionCapabilityEntry = {
|
|||
subagentRole?: unknown;
|
||||
subagentControlScope?: unknown;
|
||||
spawnedBy?: unknown;
|
||||
inheritedToolAllow?: unknown;
|
||||
inheritedToolDeny?: unknown;
|
||||
};
|
||||
|
||||
export type SessionCapabilityStore = Record<
|
||||
|
|
@ -36,6 +42,8 @@ export type SessionCapabilityStore = Record<
|
|||
subagentRole?: unknown;
|
||||
subagentControlScope?: unknown;
|
||||
spawnedBy?: unknown;
|
||||
inheritedToolAllow?: unknown;
|
||||
inheritedToolDeny?: unknown;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
@ -293,3 +301,43 @@ export function resolveStoredSubagentCapabilities(
|
|||
canControlChildren: controlScope === "children",
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveStoredSubagentInheritedToolDenylist(
|
||||
sessionKey: string | undefined | null,
|
||||
opts?: {
|
||||
cfg?: OpenClawConfig;
|
||||
store?: SessionCapabilityStore;
|
||||
},
|
||||
): string[] {
|
||||
const normalizedSessionKey = normalizeSubagentSessionKey(sessionKey);
|
||||
if (!normalizedSessionKey || !shouldInspectStoredSubagentEnvelope(normalizedSessionKey)) {
|
||||
return [];
|
||||
}
|
||||
const store = resolveSubagentCapabilityStore(normalizedSessionKey, opts);
|
||||
const entry = resolveSessionCapabilityEntry({
|
||||
sessionKey: normalizedSessionKey,
|
||||
cfg: opts?.cfg,
|
||||
store,
|
||||
});
|
||||
return normalizeInheritedToolDenylist(entry?.inheritedToolDeny);
|
||||
}
|
||||
|
||||
export function resolveStoredSubagentInheritedToolAllowlist(
|
||||
sessionKey: string | undefined | null,
|
||||
opts?: {
|
||||
cfg?: OpenClawConfig;
|
||||
store?: SessionCapabilityStore;
|
||||
},
|
||||
): string[] {
|
||||
const normalizedSessionKey = normalizeSubagentSessionKey(sessionKey);
|
||||
if (!normalizedSessionKey || !shouldInspectStoredSubagentEnvelope(normalizedSessionKey)) {
|
||||
return [];
|
||||
}
|
||||
const store = resolveSubagentCapabilityStore(normalizedSessionKey, opts);
|
||||
const entry = resolveSessionCapabilityEntry({
|
||||
sessionKey: normalizedSessionKey,
|
||||
cfg: opts?.cfg,
|
||||
store,
|
||||
});
|
||||
return normalizeInheritedToolAllowlist(entry?.inheritedToolAllow);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,30 @@ describe("subagent spawn depth + child limits", () => {
|
|||
expect(typeof childSession?.spawnedWorkspaceDir).toBe("string");
|
||||
});
|
||||
|
||||
it("persists inherited tool denies on spawned child sessions", async () => {
|
||||
hoisted.configOverride = createDepthLimitConfig({ maxSpawnDepth: 2 });
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "hello",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
workspaceDir: "/tmp/workspace-main",
|
||||
inheritedToolAllowlist: ["sessions_spawn", "read", ""],
|
||||
inheritedToolDenylist: ["bash", "exec", "read", ""],
|
||||
},
|
||||
);
|
||||
|
||||
const accepted = expectAccepted(result, "run-1");
|
||||
const childSession = persistedStore?.[accepted.childSessionKey];
|
||||
if (!childSession) {
|
||||
throw new Error("Expected persisted child session");
|
||||
}
|
||||
expect(childSession.inheritedToolAllow).toEqual(["sessions_spawn", "read"]);
|
||||
expect(childSession.inheritedToolDeny).toEqual(["exec", "read"]);
|
||||
});
|
||||
|
||||
it("rejects callers when stored spawn depth is already at the configured max", async () => {
|
||||
hoisted.configOverride = createDepthLimitConfig({ maxSpawnDepth: 2 });
|
||||
hoisted.depthBySession.set("agent:main:subagent:flat-depth-2", 2);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ import { normalizeOptionalString } from "../shared/string-coerce.js";
|
|||
import type { DeliveryContext } from "../utils/delivery-context.types.js";
|
||||
import { resolveAgentDir } from "./agent-scope-config.js";
|
||||
import type { BootstrapContextMode } from "./bootstrap-files.js";
|
||||
import {
|
||||
inheritedToolAllowPatch,
|
||||
inheritedToolDenyPatch,
|
||||
normalizeInheritedToolAllowlist,
|
||||
normalizeInheritedToolDenylist,
|
||||
} from "./inherited-tool-deny.js";
|
||||
import {
|
||||
mapToolContextToSpawnedRunMetadata,
|
||||
normalizeSpawnedRunMetadata,
|
||||
|
|
@ -151,6 +157,8 @@ export type SpawnSubagentContext = {
|
|||
requesterAgentIdOverride?: string;
|
||||
/** Explicit workspace directory for subagent to inherit (optional). */
|
||||
workspaceDir?: string;
|
||||
inheritedToolAllowlist?: string[];
|
||||
inheritedToolDenylist?: string[];
|
||||
};
|
||||
|
||||
export type SpawnSubagentResult = {
|
||||
|
|
@ -239,6 +247,14 @@ function buildDirectChildSessionPatch(patch: Record<string, unknown>): Partial<S
|
|||
if (typeof patch.spawnedWorkspaceDir === "string" && patch.spawnedWorkspaceDir.trim()) {
|
||||
entry.spawnedWorkspaceDir = patch.spawnedWorkspaceDir.trim();
|
||||
}
|
||||
const inheritedToolDeny = normalizeInheritedToolDenylist(patch.inheritedToolDeny);
|
||||
if (inheritedToolDeny.length > 0) {
|
||||
entry.inheritedToolDeny = inheritedToolDeny;
|
||||
}
|
||||
const inheritedToolAllow = normalizeInheritedToolAllowlist(patch.inheritedToolAllow);
|
||||
if (inheritedToolAllow.length > 0) {
|
||||
entry.inheritedToolAllow = inheritedToolAllow;
|
||||
}
|
||||
if (typeof patch.thinkingLevel === "string" && patch.thinkingLevel.trim()) {
|
||||
entry.thinkingLevel = patch.thinkingLevel.trim();
|
||||
}
|
||||
|
|
@ -899,6 +915,8 @@ export async function spawnSubagentDirect(
|
|||
spawnDepth: childDepth,
|
||||
subagentRole: childCapabilities.role === "main" ? null : childCapabilities.role,
|
||||
subagentControlScope: childCapabilities.controlScope,
|
||||
...inheritedToolAllowPatch(ctx.inheritedToolAllowlist),
|
||||
...inheritedToolDenyPatch(ctx.inheritedToolDenylist),
|
||||
...plan.initialSessionPatch,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,36 @@ export type AllowlistResolution = {
|
|||
|
||||
export const DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY = "__openclaw_default_plugin_tools__";
|
||||
|
||||
export function hasRestrictiveAllowPolicy(policy?: { allow?: string[] }): boolean {
|
||||
return (
|
||||
Array.isArray(policy?.allow) &&
|
||||
policy.allow.some((entry) => {
|
||||
const normalized = normalizeToolName(entry);
|
||||
return (
|
||||
Boolean(normalized) &&
|
||||
normalized !== "*" &&
|
||||
normalized !== DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function replaceWithEffectiveToolAllowlist(
|
||||
target: string[],
|
||||
tools: Array<{ name: string }>,
|
||||
): void {
|
||||
target.length = 0;
|
||||
const seen = new Set<string>();
|
||||
for (const tool of tools) {
|
||||
const normalized = normalizeToolName(tool.name);
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
export function collectExplicitAllowlist(policies: Array<ToolPolicyLike | undefined>): string[] {
|
||||
const entries: string[] = [];
|
||||
for (const policy of policies) {
|
||||
|
|
|
|||
|
|
@ -311,6 +311,34 @@ describe("sessions_spawn tool", () => {
|
|||
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes inherited tool denies to subagent spawns", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
inheritedToolDenylist: ["exec", "read"],
|
||||
});
|
||||
|
||||
await tool.execute("call-inherited-deny", {
|
||||
task: "build feature",
|
||||
});
|
||||
|
||||
const spawnContext = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 1, "spawnSubagentDirect");
|
||||
expect(spawnContext.inheritedToolDenylist).toEqual(["exec", "read"]);
|
||||
});
|
||||
|
||||
it("passes inherited tool allow lists to subagent spawns", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
inheritedToolAllowlist: ["sessions_spawn", "read"],
|
||||
});
|
||||
|
||||
await tool.execute("call-inherited-allow", {
|
||||
task: "build feature",
|
||||
});
|
||||
|
||||
const spawnContext = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 1, "spawnSubagentDirect");
|
||||
expect(spawnContext.inheritedToolAllowlist).toEqual(["sessions_spawn", "read"]);
|
||||
});
|
||||
|
||||
it("accepts taskName as a stable subagent handle", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
|
|
@ -504,6 +532,123 @@ describe("sessions_spawn tool", () => {
|
|||
expect(hoisted.registerSubagentRunMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes inherited tool denies to ACP spawns", async () => {
|
||||
registerAcpBackendForTest();
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
inheritedToolDenylist: ["custom_control_tool"],
|
||||
});
|
||||
|
||||
await tool.execute("call-acp-inherited-deny", {
|
||||
runtime: "acp",
|
||||
task: "investigate",
|
||||
agentId: "codex",
|
||||
});
|
||||
|
||||
const spawnContext = mockCallArg(hoisted.spawnAcpDirectMock, 0, 1, "spawnAcpDirect");
|
||||
expect(spawnContext.inheritedToolDenylist).toEqual(["custom_control_tool"]);
|
||||
});
|
||||
|
||||
it("rejects ACP spawns when inherited denies include command tools", async () => {
|
||||
registerAcpBackendForTest();
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
inheritedToolDenylist: ["exec"],
|
||||
});
|
||||
|
||||
const result = await tool.execute("call-acp-inherited-command-deny", {
|
||||
runtime: "acp",
|
||||
task: "investigate",
|
||||
agentId: "codex",
|
||||
});
|
||||
|
||||
expectDetailFields(result.details, { status: "forbidden", role: "codex" });
|
||||
expect(JSON.stringify(result.details)).toContain("requester denies exec");
|
||||
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects ACP spawns when inherited deny groups or patterns include command tools", async () => {
|
||||
registerAcpBackendForTest();
|
||||
const cases = [
|
||||
{ inheritedToolDenylist: ["group:fs"], expected: "requester denies apply_patch" },
|
||||
{ inheritedToolDenylist: ["group:runtime"], expected: "requester denies exec" },
|
||||
{ inheritedToolDenylist: ["exec*"], expected: "requester denies exec" },
|
||||
{ inheritedToolDenylist: ["*"], expected: "requester denies apply_patch" },
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
inheritedToolDenylist: testCase.inheritedToolDenylist,
|
||||
});
|
||||
|
||||
const result = await tool.execute("call-acp-inherited-command-group-deny", {
|
||||
runtime: "acp",
|
||||
task: "investigate",
|
||||
agentId: "codex",
|
||||
});
|
||||
|
||||
expectDetailFields(result.details, { status: "forbidden", role: "codex" });
|
||||
expect(JSON.stringify(result.details)).toContain(testCase.expected);
|
||||
}
|
||||
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects ACP spawns when inherited allows omit command tools", async () => {
|
||||
registerAcpBackendForTest();
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
inheritedToolAllowlist: ["sessions_spawn", "custom_plugin_tool"],
|
||||
});
|
||||
|
||||
const result = await tool.execute("call-acp-inherited-command-allow", {
|
||||
runtime: "acp",
|
||||
task: "investigate",
|
||||
agentId: "codex",
|
||||
});
|
||||
|
||||
expectDetailFields(result.details, { status: "forbidden", role: "codex" });
|
||||
expect(JSON.stringify(result.details)).toContain("requester does not allow apply_patch");
|
||||
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts ACP spawns when inherited allows include OpenClaw command tools", async () => {
|
||||
registerAcpBackendForTest();
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
inheritedToolAllowlist: [
|
||||
"apply_patch",
|
||||
"edit",
|
||||
"exec",
|
||||
"process",
|
||||
"read",
|
||||
"sessions_spawn",
|
||||
"write",
|
||||
],
|
||||
});
|
||||
|
||||
const result = await tool.execute("call-acp-inherited-command-allow-compatible", {
|
||||
runtime: "acp",
|
||||
task: "investigate",
|
||||
agentId: "codex",
|
||||
});
|
||||
|
||||
expectDetailFields(result.details, {
|
||||
status: "accepted",
|
||||
childSessionKey: "agent:codex:acp:1",
|
||||
});
|
||||
const spawnContext = mockCallArg(hoisted.spawnAcpDirectMock, 0, 1, "spawnAcpDirect");
|
||||
expect(spawnContext.inheritedToolAllowlist).toEqual([
|
||||
"apply_patch",
|
||||
"edit",
|
||||
"exec",
|
||||
"process",
|
||||
"read",
|
||||
"sessions_spawn",
|
||||
"write",
|
||||
]);
|
||||
});
|
||||
|
||||
it("forwards model override to ACP runtime spawns", async () => {
|
||||
registerAcpBackendForTest();
|
||||
const tool = createSessionsSpawnTool({
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ import { callGateway } from "../../gateway/call.js";
|
|||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { normalizeDeliveryContext } from "../../utils/delivery-context.shared.js";
|
||||
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
findAcpUnsupportedInheritedToolAllow,
|
||||
findAcpUnsupportedInheritedToolDeny,
|
||||
formatAcpInheritedToolAllowError,
|
||||
formatAcpInheritedToolDenyError,
|
||||
} from "../inherited-tool-deny.js";
|
||||
import { optionalStringEnum } from "../schema/typebox.js";
|
||||
import type { SpawnedToolContext } from "../spawned-context.js";
|
||||
import { registerSubagentRun } from "../subagent-registry.js";
|
||||
|
|
@ -312,6 +318,28 @@ export function createSessionsSpawnTool(
|
|||
...roleContext,
|
||||
});
|
||||
}
|
||||
const acpUnsupportedInheritedTool =
|
||||
runtime === "acp"
|
||||
? findAcpUnsupportedInheritedToolDeny(opts?.inheritedToolDenylist)
|
||||
: undefined;
|
||||
if (acpUnsupportedInheritedTool) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error: formatAcpInheritedToolDenyError(acpUnsupportedInheritedTool),
|
||||
...roleContext,
|
||||
});
|
||||
}
|
||||
const acpUnsupportedInheritedAllow =
|
||||
runtime === "acp"
|
||||
? findAcpUnsupportedInheritedToolAllow(opts?.inheritedToolAllowlist)
|
||||
: undefined;
|
||||
if (acpUnsupportedInheritedAllow) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error: formatAcpInheritedToolAllowError(acpUnsupportedInheritedAllow),
|
||||
...roleContext,
|
||||
});
|
||||
}
|
||||
if (runtime === "acp" && lightContext) {
|
||||
throw new Error("lightContext is only supported for runtime='subagent'.");
|
||||
}
|
||||
|
|
@ -374,6 +402,8 @@ export function createSessionsSpawnTool(
|
|||
agentGroupSpace: opts?.agentGroupSpace,
|
||||
agentMemberRoleIds: opts?.agentMemberRoleIds,
|
||||
sandboxed: opts?.sandboxed,
|
||||
inheritedToolAllowlist: opts?.inheritedToolAllowlist,
|
||||
inheritedToolDenylist: opts?.inheritedToolDenylist,
|
||||
},
|
||||
);
|
||||
const childSessionKey = result.childSessionKey?.trim();
|
||||
|
|
@ -477,6 +507,8 @@ export function createSessionsSpawnTool(
|
|||
agentMemberRoleIds: opts?.agentMemberRoleIds,
|
||||
requesterAgentIdOverride: opts?.requesterAgentIdOverride,
|
||||
workspaceDir: opts?.workspaceDir,
|
||||
inheritedToolAllowlist: opts?.inheritedToolAllowlist,
|
||||
inheritedToolDenylist: opts?.inheritedToolDenylist,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
isToolAllowedByPolicies,
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveInheritedToolPolicyForSession,
|
||||
resolveSubagentToolPolicyForSession,
|
||||
} from "../../agents/pi-tools.policy.js";
|
||||
import {
|
||||
|
|
@ -760,6 +761,9 @@ export async function dispatchReplyFromConfig(
|
|||
store: subagentStore,
|
||||
})
|
||||
: undefined;
|
||||
const inheritedToolPolicy = resolveInheritedToolPolicyForSession(cfg, acpDispatchSessionKey, {
|
||||
store: subagentStore,
|
||||
});
|
||||
const messageToolAvailable = isToolAllowedByPolicies("message", [
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
|
|
@ -769,6 +773,7 @@ export async function dispatchReplyFromConfig(
|
|||
agentPolicy,
|
||||
groupPolicy,
|
||||
subagentPolicy,
|
||||
inheritedToolPolicy,
|
||||
]);
|
||||
const sourceReplyPolicy = resolveSourceReplyVisibilityPolicy({
|
||||
cfg,
|
||||
|
|
|
|||
|
|
@ -210,6 +210,10 @@ export type SessionEntry = {
|
|||
subagentRole?: "orchestrator" | "leaf";
|
||||
/** Explicit control scope assigned at spawn time for subagent control decisions. */
|
||||
subagentControlScope?: "children" | "none";
|
||||
/** Session-scoped tool deny entries inherited from the caller that created this session. */
|
||||
inheritedToolDeny?: string[];
|
||||
/** Session-scoped tool allow entries inherited from the caller that created this session. */
|
||||
inheritedToolAllow?: string[];
|
||||
/** Plugin id that created this session through api.runtime.subagent. */
|
||||
pluginOwnerId?: string;
|
||||
systemSent?: boolean;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type ScopedToolsCall = {
|
|||
messageProvider?: string;
|
||||
senderIsOwner?: boolean;
|
||||
surface?: string;
|
||||
excludeToolNames?: Iterable<string>;
|
||||
};
|
||||
|
||||
type BeforeToolCallHookInput = {
|
||||
|
|
@ -180,6 +181,14 @@ describe("mcp loopback server", () => {
|
|||
expect(call.messageProvider).toBe("telegram");
|
||||
expect(call.senderIsOwner).toBe(false);
|
||||
expect(call.surface).toBe("loopback");
|
||||
expect(Array.from(call.excludeToolNames ?? [])).toEqual([
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"apply_patch",
|
||||
"exec",
|
||||
"process",
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds empty properties for object schemas that omit properties", async () => {
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ export const SessionsPatchParamsSchema = Type.Object(
|
|||
subagentControlScope: Type.Optional(
|
||||
Type.Union([Type.Literal("children"), Type.Literal("none"), Type.Null()]),
|
||||
),
|
||||
inheritedToolAllow: Type.Optional(Type.Union([Type.Array(NonEmptyString), Type.Null()])),
|
||||
inheritedToolDeny: Type.Optional(Type.Union([Type.Array(NonEmptyString), Type.Null()])),
|
||||
sendPolicy: Type.Optional(
|
||||
Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -486,6 +486,44 @@ describe("gateway sessions patch", () => {
|
|||
expect(entry.spawnDepth).toBe(2);
|
||||
});
|
||||
|
||||
test("sets inheritedToolDeny for ACP sessions", async () => {
|
||||
const entry = expectPatchOk(
|
||||
await runPatch({
|
||||
storeKey: "agent:main:acp:child",
|
||||
patch: { key: "agent:main:acp:child", inheritedToolDeny: ["bash", "read", "bash"] },
|
||||
}),
|
||||
);
|
||||
expect(entry.inheritedToolDeny).toEqual(["exec", "read"]);
|
||||
});
|
||||
|
||||
test("sets inheritedToolAllow for ACP sessions", async () => {
|
||||
const entry = expectPatchOk(
|
||||
await runPatch({
|
||||
storeKey: "agent:main:acp:child",
|
||||
patch: {
|
||||
key: "agent:main:acp:child",
|
||||
inheritedToolAllow: ["sessions_spawn", "read", "sessions_spawn"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(entry.inheritedToolAllow).toEqual(["sessions_spawn", "read"]);
|
||||
});
|
||||
|
||||
test("preserves inheritedToolDeny entries beyond large configured lists", async () => {
|
||||
const configuredDeny = Array.from({ length: 150 }, (_, index) => `custom_${index}`);
|
||||
const entry = expectPatchOk(
|
||||
await runPatch({
|
||||
storeKey: "agent:main:subagent:child",
|
||||
patch: {
|
||||
key: "agent:main:subagent:child",
|
||||
inheritedToolDeny: [...configuredDeny, "exec"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(entry.inheritedToolDeny).toHaveLength(151);
|
||||
expect(entry.inheritedToolDeny?.at(-1)).toBe("exec");
|
||||
});
|
||||
|
||||
test("rejects spawnDepth on non-subagent sessions", async () => {
|
||||
const result = await runPatch({
|
||||
patch: { key: MAIN_SESSION_KEY, spawnDepth: 1 },
|
||||
|
|
@ -500,6 +538,20 @@ describe("gateway sessions patch", () => {
|
|||
expectPatchError(result, "spawnedWorkspaceDir is only supported");
|
||||
});
|
||||
|
||||
test("rejects inheritedToolDeny on non-subagent sessions", async () => {
|
||||
const result = await runPatch({
|
||||
patch: { key: MAIN_SESSION_KEY, inheritedToolDeny: ["exec"] },
|
||||
});
|
||||
expectPatchError(result, "inheritedToolDeny is only supported");
|
||||
});
|
||||
|
||||
test("rejects inheritedToolAllow on non-subagent sessions", async () => {
|
||||
const result = await runPatch({
|
||||
patch: { key: MAIN_SESSION_KEY, inheritedToolAllow: ["read"] },
|
||||
});
|
||||
expectPatchError(result, "inheritedToolAllow is only supported");
|
||||
});
|
||||
|
||||
test("normalizes exec/send/group patches", async () => {
|
||||
const entry = expectPatchOk(
|
||||
await runPatch({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import {
|
||||
normalizeInheritedToolAllowlist,
|
||||
normalizeInheritedToolDenylist,
|
||||
} from "../agents/inherited-tool-deny.js";
|
||||
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
|
||||
import {
|
||||
resolveAllowedModelRef,
|
||||
|
|
@ -228,6 +232,46 @@ export async function applySessionsPatchToStore(params: {
|
|||
}
|
||||
}
|
||||
|
||||
if ("inheritedToolDeny" in patch) {
|
||||
const raw = patch.inheritedToolDeny;
|
||||
if (raw === null) {
|
||||
delete next.inheritedToolDeny;
|
||||
} else if (raw !== undefined) {
|
||||
if (!Array.isArray(raw)) {
|
||||
return invalid("invalid inheritedToolDeny (use an array of tool names)");
|
||||
}
|
||||
if (!supportsSpawnLineage(storeKey)) {
|
||||
return invalid("inheritedToolDeny is only supported for subagent:* or acp:* sessions");
|
||||
}
|
||||
const inheritedToolDeny = normalizeInheritedToolDenylist(raw);
|
||||
if (inheritedToolDeny.length > 0) {
|
||||
next.inheritedToolDeny = inheritedToolDeny;
|
||||
} else {
|
||||
delete next.inheritedToolDeny;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("inheritedToolAllow" in patch) {
|
||||
const raw = patch.inheritedToolAllow;
|
||||
if (raw === null) {
|
||||
delete next.inheritedToolAllow;
|
||||
} else if (raw !== undefined) {
|
||||
if (!Array.isArray(raw)) {
|
||||
return invalid("invalid inheritedToolAllow (use an array of tool names)");
|
||||
}
|
||||
if (!supportsSpawnLineage(storeKey)) {
|
||||
return invalid("inheritedToolAllow is only supported for subagent:* or acp:* sessions");
|
||||
}
|
||||
const inheritedToolAllow = normalizeInheritedToolAllowlist(raw);
|
||||
if (inheritedToolAllow.length > 0) {
|
||||
next.inheritedToolAllow = inheritedToolAllow;
|
||||
} else {
|
||||
delete next.inheritedToolAllow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("label" in patch) {
|
||||
const raw = patch.label;
|
||||
if (raw === null) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createOpenClawTools } from "../agents/openclaw-tools.js";
|
|||
import {
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveInheritedToolPolicyForSession,
|
||||
resolveSubagentToolPolicyForSession,
|
||||
} from "../agents/pi-tools.policy.js";
|
||||
import {
|
||||
|
|
@ -16,7 +17,9 @@ import {
|
|||
import {
|
||||
collectExplicitAllowlist,
|
||||
collectExplicitDenylist,
|
||||
hasRestrictiveAllowPolicy,
|
||||
mergeAlsoAllowPolicy,
|
||||
replaceWithEffectiveToolAllowlist,
|
||||
resolveToolProfilePolicy,
|
||||
} from "../agents/tool-policy.js";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
|
|
@ -81,10 +84,50 @@ export function resolveGatewayScopedTools(params: {
|
|||
store: subagentStore,
|
||||
})
|
||||
: undefined;
|
||||
const inheritedToolPolicy = resolveInheritedToolPolicyForSession(params.cfg, params.sessionKey, {
|
||||
store: subagentStore,
|
||||
});
|
||||
const excludedToolNames = params.excludeToolNames ? Array.from(params.excludeToolNames) : [];
|
||||
const surface = params.surface ?? "http";
|
||||
const gatewayToolsCfg = params.cfg.gateway?.tools;
|
||||
const defaultGatewayDeny =
|
||||
surface === "http"
|
||||
? DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter((name) => !gatewayToolsCfg?.allow?.includes(name))
|
||||
: [];
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
params.cfg,
|
||||
agentId ?? resolveDefaultAgentId(params.cfg),
|
||||
);
|
||||
const explicitDenylist = collectExplicitDenylist([
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
subagentPolicy,
|
||||
inheritedToolPolicy,
|
||||
defaultGatewayDeny.length > 0 ? { deny: defaultGatewayDeny } : undefined,
|
||||
Array.isArray(gatewayToolsCfg?.deny) ? { deny: gatewayToolsCfg.deny } : undefined,
|
||||
excludedToolNames.length > 0 ? { deny: excludedToolNames } : undefined,
|
||||
]);
|
||||
const inheritedToolDenylist = [...explicitDenylist];
|
||||
// Passed by reference to sessions_spawn and populated after the final policy
|
||||
// pass so child sessions inherit the actual parent tool surface.
|
||||
const inheritedToolAllowlist: string[] = [];
|
||||
const shouldInheritEffectiveToolAllowlist = [
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
subagentPolicy,
|
||||
inheritedToolPolicy,
|
||||
gatewayRequestedTools.length > 0 ? { allow: gatewayRequestedTools } : undefined,
|
||||
].some(hasRestrictiveAllowPolicy);
|
||||
|
||||
const allTools = createOpenClawTools({
|
||||
agentSessionKey: params.sessionKey,
|
||||
|
|
@ -108,18 +151,12 @@ export function resolveGatewayScopedTools(params: {
|
|||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
subagentPolicy,
|
||||
inheritedToolPolicy,
|
||||
gatewayRequestedTools.length > 0 ? { allow: gatewayRequestedTools } : undefined,
|
||||
]),
|
||||
pluginToolDenylist: collectExplicitDenylist([
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
subagentPolicy,
|
||||
]),
|
||||
pluginToolDenylist: explicitDenylist,
|
||||
inheritedToolAllowlist,
|
||||
inheritedToolDenylist,
|
||||
});
|
||||
|
||||
const policyFiltered = applyToolPolicyPipeline({
|
||||
|
|
@ -142,23 +179,22 @@ export function resolveGatewayScopedTools(params: {
|
|||
agentId,
|
||||
}),
|
||||
{ policy: subagentPolicy, label: "subagent tools.allow" },
|
||||
{ policy: inheritedToolPolicy, label: "inherited tools" },
|
||||
],
|
||||
});
|
||||
|
||||
const surface = params.surface ?? "http";
|
||||
const gatewayToolsCfg = params.cfg.gateway?.tools;
|
||||
const defaultGatewayDeny =
|
||||
surface === "http"
|
||||
? DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter((name) => !gatewayToolsCfg?.allow?.includes(name))
|
||||
: [];
|
||||
const gatewayDenySet = new Set([
|
||||
...defaultGatewayDeny,
|
||||
...(Array.isArray(gatewayToolsCfg?.deny) ? gatewayToolsCfg.deny : []),
|
||||
...(params.excludeToolNames ? Array.from(params.excludeToolNames) : []),
|
||||
...excludedToolNames,
|
||||
]);
|
||||
const tools = policyFiltered.filter((tool) => !gatewayDenySet.has(tool.name));
|
||||
if (shouldInheritEffectiveToolAllowlist) {
|
||||
replaceWithEffectiveToolAllowlist(inheritedToolAllowlist, tools);
|
||||
}
|
||||
|
||||
return {
|
||||
agentId,
|
||||
tools: policyFiltered.filter((tool) => !gatewayDenySet.has(tool.name)),
|
||||
tools,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [
|
|||
"spawnDepth",
|
||||
"subagentRole",
|
||||
"subagentControlScope",
|
||||
"inheritedToolDeny",
|
||||
"inheritedToolAllow",
|
||||
"subagentRecovery",
|
||||
"pluginOwnerId",
|
||||
"systemSent",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue