feat(sessions): add toolPolicy param to sessions_spawn for sub-agent tool restrictions
Add a toolPolicy parameter to sessions_spawn that allows the parent agent to restrict the tool set available to spawned sub-agents. The new toolPolicy parameter accepts allow and deny arrays: - allow: Narrows the sub-agent to only these tools (on top of default deny list) - deny: Additional tools/groups to block (appended to default deny list) Supports tool group syntax (group:web, group:fs, group:runtime, etc.) Implementation: - SessionEntry gains spawnToolPolicy field (persisted per-session) - sessions.patch accepts spawnToolPolicy for subagent sessions - Policy is immutable once set (cannot be changed after spawn) - Merges with existing default subagent deny list - Applied in both embedded agent path and HTTP tool invoke path
This commit is contained in:
parent
4583f88626
commit
a55b4dcb7e
@ -313,6 +313,7 @@ export async function runEmbeddedPiAgent(
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
spawnToolPolicy: params.spawnToolPolicy,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
replyToMode: params.replyToMode,
|
||||
|
||||
@ -215,6 +215,7 @@ export async function runEmbeddedAttempt(
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
spawnToolPolicy: params.spawnToolPolicy,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
|
||||
@ -35,6 +35,8 @@ export type RunEmbeddedPiAgentParams = {
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
/** Per-spawn tool policy override from sessions_spawn toolPolicy param. */
|
||||
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
|
||||
@ -31,6 +31,8 @@ export type EmbeddedRunAttemptParams = {
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
/** Per-spawn tool policy override from sessions_spawn toolPolicy param. */
|
||||
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
|
||||
@ -150,6 +150,8 @@ export function createMoltbotCodingTools(options?: {
|
||||
hasRepliedRef?: { value: boolean };
|
||||
/** If true, the model has native vision capability */
|
||||
modelHasVision?: boolean;
|
||||
/** Per-spawn tool policy override from sessions_spawn toolPolicy param. */
|
||||
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||
}): AnyAgentTool[] {
|
||||
const execToolName = "exec";
|
||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||
@ -197,10 +199,27 @@ export function createMoltbotCodingTools(options?: {
|
||||
providerProfileAlsoAllow,
|
||||
);
|
||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
const subagentPolicy =
|
||||
const baseSubagentPolicy =
|
||||
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
||||
? resolveSubagentToolPolicy(options.config)
|
||||
: undefined;
|
||||
const subagentPolicy = (() => {
|
||||
if (!baseSubagentPolicy) return undefined;
|
||||
const spawnPolicy = options?.spawnToolPolicy;
|
||||
if (!spawnPolicy) return baseSubagentPolicy;
|
||||
// Merge spawn-time policy: spawn allow narrows default, spawn deny appends to default.
|
||||
const mergedDeny = [
|
||||
...(baseSubagentPolicy.deny ?? []),
|
||||
...(spawnPolicy.deny ?? []),
|
||||
];
|
||||
const mergedAllow = spawnPolicy.allow
|
||||
? spawnPolicy.allow
|
||||
: baseSubagentPolicy.allow;
|
||||
return {
|
||||
allow: mergedAllow,
|
||||
deny: mergedDeny.length > 0 ? mergedDeny : undefined,
|
||||
};
|
||||
})();
|
||||
const allowBackground = isToolAllowedByPolicies("process", [
|
||||
profilePolicyWithAlsoAllow,
|
||||
providerProfilePolicyWithAlsoAllow,
|
||||
|
||||
@ -31,6 +31,28 @@ const SessionsSpawnToolSchema = Type.Object({
|
||||
agentId: Type.Optional(Type.String()),
|
||||
model: Type.Optional(Type.String()),
|
||||
thinking: Type.Optional(Type.String()),
|
||||
toolPolicy: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
allow: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description:
|
||||
'Tool names or groups to allow (e.g. "web_fetch", "read", "group:web"). When set, only these tools are available.',
|
||||
}),
|
||||
),
|
||||
deny: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description:
|
||||
'Tool names or groups to deny (e.g. "exec", "group:runtime"). Appended to default subagent deny list.',
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
description:
|
||||
"Restrict available tools for the sub-agent. allow narrows to listed tools; deny blocks listed tools (additive with defaults).",
|
||||
},
|
||||
),
|
||||
),
|
||||
runTimeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
// Back-compat alias. Prefer runTimeoutSeconds.
|
||||
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
@ -83,6 +105,10 @@ export function createSessionsSpawnTool(opts?: {
|
||||
const requestedAgentId = readStringParam(params, "agentId");
|
||||
const modelOverride = readStringParam(params, "model");
|
||||
const thinkingOverrideRaw = readStringParam(params, "thinking");
|
||||
const toolPolicy =
|
||||
params.toolPolicy && typeof params.toolPolicy === "object"
|
||||
? (params.toolPolicy as { allow?: string[]; deny?: string[] })
|
||||
: undefined;
|
||||
const cleanup =
|
||||
params.cleanup === "keep" || params.cleanup === "delete"
|
||||
? (params.cleanup as "keep" | "delete")
|
||||
@ -200,6 +226,23 @@ export function createSessionsSpawnTool(opts?: {
|
||||
modelWarning = messageText;
|
||||
}
|
||||
}
|
||||
if (toolPolicy && (toolPolicy.allow || toolPolicy.deny)) {
|
||||
try {
|
||||
await callGateway({
|
||||
method: "sessions.patch",
|
||||
params: { key: childSessionKey, spawnToolPolicy: toolPolicy },
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
} catch (err) {
|
||||
const messageText =
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
||||
return jsonResult({
|
||||
status: "error",
|
||||
error: `Failed to apply toolPolicy: ${messageText}`,
|
||||
childSessionKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
const childSystemPrompt = buildSubagentSystemPrompt({
|
||||
requesterSessionKey,
|
||||
requesterOrigin,
|
||||
|
||||
@ -418,6 +418,7 @@ export async function agentCommand(
|
||||
groupChannel: runContext.groupChannel,
|
||||
groupSpace: runContext.groupSpace,
|
||||
spawnedBy,
|
||||
spawnToolPolicy: sessionEntry?.spawnToolPolicy,
|
||||
currentChannelId: runContext.currentChannelId,
|
||||
currentThreadTs: runContext.currentThreadTs,
|
||||
replyToMode: runContext.replyToMode,
|
||||
|
||||
@ -36,6 +36,8 @@ export type SessionEntry = {
|
||||
sessionFile?: string;
|
||||
/** Parent session key that spawned this session (used for sandbox session-tool scoping). */
|
||||
spawnedBy?: string;
|
||||
/** Per-spawn tool policy override. Set by sessions_spawn to restrict child session tools. */
|
||||
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||
systemSent?: boolean;
|
||||
abortedLastRun?: boolean;
|
||||
chatType?: SessionChatType;
|
||||
|
||||
@ -72,6 +72,15 @@ export const SessionsPatchParamsSchema = Type.Object(
|
||||
execNode: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
spawnToolPolicy: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Object({
|
||||
allow: Type.Optional(Type.Array(Type.String())),
|
||||
deny: Type.Optional(Type.Array(Type.String())),
|
||||
}),
|
||||
Type.Null(),
|
||||
]),
|
||||
),
|
||||
sendPolicy: Type.Optional(
|
||||
Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]),
|
||||
),
|
||||
|
||||
@ -247,6 +247,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
providerOverride: entry?.providerOverride,
|
||||
label: labelValue,
|
||||
spawnedBy: spawnedByValue,
|
||||
spawnToolPolicy: entry?.spawnToolPolicy,
|
||||
channel: entry?.channel ?? request.channel?.trim(),
|
||||
groupId: resolvedGroupId ?? entry?.groupId,
|
||||
groupChannel: resolvedGroupChannel ?? entry?.groupChannel,
|
||||
|
||||
@ -90,6 +90,30 @@ export async function applySessionsPatchToStore(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if ("spawnToolPolicy" in patch) {
|
||||
const raw = patch.spawnToolPolicy;
|
||||
if (raw === null) {
|
||||
delete next.spawnToolPolicy;
|
||||
} else if (raw !== undefined) {
|
||||
if (!isSubagentSessionKey(storeKey)) {
|
||||
return invalid("spawnToolPolicy is only supported for subagent:* sessions");
|
||||
}
|
||||
if (existing?.spawnToolPolicy) {
|
||||
return invalid("spawnToolPolicy cannot be changed once set");
|
||||
}
|
||||
const policy: { allow?: string[]; deny?: string[] } = {};
|
||||
if (Array.isArray(raw.allow) && raw.allow.length > 0) {
|
||||
policy.allow = raw.allow.filter((v: unknown) => typeof v === "string" && v.trim()).map((v: string) => v.trim());
|
||||
}
|
||||
if (Array.isArray(raw.deny) && raw.deny.length > 0) {
|
||||
policy.deny = raw.deny.filter((v: unknown) => typeof v === "string" && v.trim()).map((v: string) => v.trim());
|
||||
}
|
||||
if (policy.allow || policy.deny) {
|
||||
next.spawnToolPolicy = policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("label" in patch) {
|
||||
const raw = patch.label;
|
||||
if (raw === null) {
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
} from "../agents/tool-policy.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||
import { loadSessionEntry } from "./session-utils.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js";
|
||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||
@ -189,9 +190,28 @@ export async function handleToolsInvokeHttpRequest(
|
||||
messageProvider: messageChannel ?? undefined,
|
||||
accountId: accountId ?? null,
|
||||
});
|
||||
const subagentPolicy = isSubagentSessionKey(sessionKey)
|
||||
const baseSubagentPolicy = isSubagentSessionKey(sessionKey)
|
||||
? resolveSubagentToolPolicy(cfg)
|
||||
: undefined;
|
||||
const subagentPolicy = (() => {
|
||||
if (!baseSubagentPolicy) return undefined;
|
||||
let spawnPolicy: { allow?: string[]; deny?: string[] } | undefined;
|
||||
try {
|
||||
spawnPolicy = loadSessionEntry(sessionKey)?.entry?.spawnToolPolicy;
|
||||
} catch {
|
||||
spawnPolicy = undefined;
|
||||
}
|
||||
if (!spawnPolicy) return baseSubagentPolicy;
|
||||
const mergedDeny = [
|
||||
...(baseSubagentPolicy.deny ?? []),
|
||||
...(spawnPolicy.deny ?? []),
|
||||
];
|
||||
const mergedAllow = spawnPolicy.allow ? spawnPolicy.allow : baseSubagentPolicy.allow;
|
||||
return {
|
||||
allow: mergedAllow,
|
||||
deny: mergedDeny.length > 0 ? mergedDeny : undefined,
|
||||
};
|
||||
})();
|
||||
|
||||
// Build tool list (core + plugin tools).
|
||||
const allTools = createMoltbotTools({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user