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,
|
groupChannel: params.groupChannel,
|
||||||
groupSpace: params.groupSpace,
|
groupSpace: params.groupSpace,
|
||||||
spawnedBy: params.spawnedBy,
|
spawnedBy: params.spawnedBy,
|
||||||
|
spawnToolPolicy: params.spawnToolPolicy,
|
||||||
currentChannelId: params.currentChannelId,
|
currentChannelId: params.currentChannelId,
|
||||||
currentThreadTs: params.currentThreadTs,
|
currentThreadTs: params.currentThreadTs,
|
||||||
replyToMode: params.replyToMode,
|
replyToMode: params.replyToMode,
|
||||||
|
|||||||
@ -215,6 +215,7 @@ export async function runEmbeddedAttempt(
|
|||||||
groupChannel: params.groupChannel,
|
groupChannel: params.groupChannel,
|
||||||
groupSpace: params.groupSpace,
|
groupSpace: params.groupSpace,
|
||||||
spawnedBy: params.spawnedBy,
|
spawnedBy: params.spawnedBy,
|
||||||
|
spawnToolPolicy: params.spawnToolPolicy,
|
||||||
senderId: params.senderId,
|
senderId: params.senderId,
|
||||||
senderName: params.senderName,
|
senderName: params.senderName,
|
||||||
senderUsername: params.senderUsername,
|
senderUsername: params.senderUsername,
|
||||||
|
|||||||
@ -35,6 +35,8 @@ export type RunEmbeddedPiAgentParams = {
|
|||||||
groupSpace?: string | null;
|
groupSpace?: string | null;
|
||||||
/** Parent session key for subagent policy inheritance. */
|
/** Parent session key for subagent policy inheritance. */
|
||||||
spawnedBy?: string | null;
|
spawnedBy?: string | null;
|
||||||
|
/** Per-spawn tool policy override from sessions_spawn toolPolicy param. */
|
||||||
|
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||||
senderId?: string | null;
|
senderId?: string | null;
|
||||||
senderName?: string | null;
|
senderName?: string | null;
|
||||||
senderUsername?: string | null;
|
senderUsername?: string | null;
|
||||||
|
|||||||
@ -31,6 +31,8 @@ export type EmbeddedRunAttemptParams = {
|
|||||||
groupSpace?: string | null;
|
groupSpace?: string | null;
|
||||||
/** Parent session key for subagent policy inheritance. */
|
/** Parent session key for subagent policy inheritance. */
|
||||||
spawnedBy?: string | null;
|
spawnedBy?: string | null;
|
||||||
|
/** Per-spawn tool policy override from sessions_spawn toolPolicy param. */
|
||||||
|
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||||
senderId?: string | null;
|
senderId?: string | null;
|
||||||
senderName?: string | null;
|
senderName?: string | null;
|
||||||
senderUsername?: string | null;
|
senderUsername?: string | null;
|
||||||
|
|||||||
@ -150,6 +150,8 @@ export function createMoltbotCodingTools(options?: {
|
|||||||
hasRepliedRef?: { value: boolean };
|
hasRepliedRef?: { value: boolean };
|
||||||
/** If true, the model has native vision capability */
|
/** If true, the model has native vision capability */
|
||||||
modelHasVision?: boolean;
|
modelHasVision?: boolean;
|
||||||
|
/** Per-spawn tool policy override from sessions_spawn toolPolicy param. */
|
||||||
|
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||||
}): AnyAgentTool[] {
|
}): AnyAgentTool[] {
|
||||||
const execToolName = "exec";
|
const execToolName = "exec";
|
||||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||||
@ -197,10 +199,27 @@ export function createMoltbotCodingTools(options?: {
|
|||||||
providerProfileAlsoAllow,
|
providerProfileAlsoAllow,
|
||||||
);
|
);
|
||||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||||
const subagentPolicy =
|
const baseSubagentPolicy =
|
||||||
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
||||||
? resolveSubagentToolPolicy(options.config)
|
? resolveSubagentToolPolicy(options.config)
|
||||||
: undefined;
|
: 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", [
|
const allowBackground = isToolAllowedByPolicies("process", [
|
||||||
profilePolicyWithAlsoAllow,
|
profilePolicyWithAlsoAllow,
|
||||||
providerProfilePolicyWithAlsoAllow,
|
providerProfilePolicyWithAlsoAllow,
|
||||||
|
|||||||
@ -31,6 +31,28 @@ const SessionsSpawnToolSchema = Type.Object({
|
|||||||
agentId: Type.Optional(Type.String()),
|
agentId: Type.Optional(Type.String()),
|
||||||
model: Type.Optional(Type.String()),
|
model: Type.Optional(Type.String()),
|
||||||
thinking: 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 })),
|
runTimeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||||
// Back-compat alias. Prefer runTimeoutSeconds.
|
// Back-compat alias. Prefer runTimeoutSeconds.
|
||||||
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||||
@ -83,6 +105,10 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
const requestedAgentId = readStringParam(params, "agentId");
|
const requestedAgentId = readStringParam(params, "agentId");
|
||||||
const modelOverride = readStringParam(params, "model");
|
const modelOverride = readStringParam(params, "model");
|
||||||
const thinkingOverrideRaw = readStringParam(params, "thinking");
|
const thinkingOverrideRaw = readStringParam(params, "thinking");
|
||||||
|
const toolPolicy =
|
||||||
|
params.toolPolicy && typeof params.toolPolicy === "object"
|
||||||
|
? (params.toolPolicy as { allow?: string[]; deny?: string[] })
|
||||||
|
: undefined;
|
||||||
const cleanup =
|
const cleanup =
|
||||||
params.cleanup === "keep" || params.cleanup === "delete"
|
params.cleanup === "keep" || params.cleanup === "delete"
|
||||||
? (params.cleanup as "keep" | "delete")
|
? (params.cleanup as "keep" | "delete")
|
||||||
@ -200,6 +226,23 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
modelWarning = messageText;
|
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({
|
const childSystemPrompt = buildSubagentSystemPrompt({
|
||||||
requesterSessionKey,
|
requesterSessionKey,
|
||||||
requesterOrigin,
|
requesterOrigin,
|
||||||
|
|||||||
@ -418,6 +418,7 @@ export async function agentCommand(
|
|||||||
groupChannel: runContext.groupChannel,
|
groupChannel: runContext.groupChannel,
|
||||||
groupSpace: runContext.groupSpace,
|
groupSpace: runContext.groupSpace,
|
||||||
spawnedBy,
|
spawnedBy,
|
||||||
|
spawnToolPolicy: sessionEntry?.spawnToolPolicy,
|
||||||
currentChannelId: runContext.currentChannelId,
|
currentChannelId: runContext.currentChannelId,
|
||||||
currentThreadTs: runContext.currentThreadTs,
|
currentThreadTs: runContext.currentThreadTs,
|
||||||
replyToMode: runContext.replyToMode,
|
replyToMode: runContext.replyToMode,
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export type SessionEntry = {
|
|||||||
sessionFile?: string;
|
sessionFile?: string;
|
||||||
/** Parent session key that spawned this session (used for sandbox session-tool scoping). */
|
/** Parent session key that spawned this session (used for sandbox session-tool scoping). */
|
||||||
spawnedBy?: string;
|
spawnedBy?: string;
|
||||||
|
/** Per-spawn tool policy override. Set by sessions_spawn to restrict child session tools. */
|
||||||
|
spawnToolPolicy?: { allow?: string[]; deny?: string[] };
|
||||||
systemSent?: boolean;
|
systemSent?: boolean;
|
||||||
abortedLastRun?: boolean;
|
abortedLastRun?: boolean;
|
||||||
chatType?: SessionChatType;
|
chatType?: SessionChatType;
|
||||||
|
|||||||
@ -72,6 +72,15 @@ export const SessionsPatchParamsSchema = Type.Object(
|
|||||||
execNode: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
execNode: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
spawnedBy: 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(
|
sendPolicy: Type.Optional(
|
||||||
Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]),
|
Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -247,6 +247,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
providerOverride: entry?.providerOverride,
|
providerOverride: entry?.providerOverride,
|
||||||
label: labelValue,
|
label: labelValue,
|
||||||
spawnedBy: spawnedByValue,
|
spawnedBy: spawnedByValue,
|
||||||
|
spawnToolPolicy: entry?.spawnToolPolicy,
|
||||||
channel: entry?.channel ?? request.channel?.trim(),
|
channel: entry?.channel ?? request.channel?.trim(),
|
||||||
groupId: resolvedGroupId ?? entry?.groupId,
|
groupId: resolvedGroupId ?? entry?.groupId,
|
||||||
groupChannel: resolvedGroupChannel ?? entry?.groupChannel,
|
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) {
|
if ("label" in patch) {
|
||||||
const raw = patch.label;
|
const raw = patch.label;
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
} from "../agents/tool-policy.js";
|
} from "../agents/tool-policy.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||||
|
import { loadSessionEntry } from "./session-utils.js";
|
||||||
import { logWarn } from "../logger.js";
|
import { logWarn } from "../logger.js";
|
||||||
import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js";
|
import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js";
|
||||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||||
@ -189,9 +190,28 @@ export async function handleToolsInvokeHttpRequest(
|
|||||||
messageProvider: messageChannel ?? undefined,
|
messageProvider: messageChannel ?? undefined,
|
||||||
accountId: accountId ?? null,
|
accountId: accountId ?? null,
|
||||||
});
|
});
|
||||||
const subagentPolicy = isSubagentSessionKey(sessionKey)
|
const baseSubagentPolicy = isSubagentSessionKey(sessionKey)
|
||||||
? resolveSubagentToolPolicy(cfg)
|
? resolveSubagentToolPolicy(cfg)
|
||||||
: undefined;
|
: 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).
|
// Build tool list (core + plugin tools).
|
||||||
const allTools = createMoltbotTools({
|
const allTools = createMoltbotTools({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user