Channels: add per-group tool policies
This commit is contained in:
committed by
Peter Steinberger
parent
e51bf46abe
commit
c07949a99c
@@ -264,6 +264,9 @@ export async function runEmbeddedPiAgent(
|
||||
agentAccountId: params.agentAccountId,
|
||||
messageTo: params.messageTo,
|
||||
messageThreadId: params.messageThreadId,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
replyToMode: params.replyToMode,
|
||||
|
||||
@@ -208,6 +208,9 @@ export async function runEmbeddedAttempt(
|
||||
agentAccountId: params.agentAccountId,
|
||||
messageTo: params.messageTo,
|
||||
messageThreadId: params.messageThreadId,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
|
||||
@@ -27,6 +27,12 @@ export type RunEmbeddedPiAgentParams = {
|
||||
messageTo?: string;
|
||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||
messageThreadId?: string | number;
|
||||
/** Group id for channel-level tool policy resolution. */
|
||||
groupId?: string | null;
|
||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||
groupChannel?: string | null;
|
||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||
groupSpace?: string | null;
|
||||
/** Current channel ID for auto-threading (Slack). */
|
||||
currentChannelId?: string;
|
||||
/** Current thread timestamp for auto-threading (Slack). */
|
||||
|
||||
@@ -23,6 +23,12 @@ export type EmbeddedRunAttemptParams = {
|
||||
agentAccountId?: string;
|
||||
messageTo?: string;
|
||||
messageThreadId?: string | number;
|
||||
/** Group id for channel-level tool policy resolution. */
|
||||
groupId?: string | null;
|
||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||
groupChannel?: string | null;
|
||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||
groupSpace?: string | null;
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
|
||||
@@ -231,6 +231,70 @@ describe("Agent-specific tool filtering", () => {
|
||||
expect(familyToolNames).not.toContain("apply_patch");
|
||||
});
|
||||
|
||||
it("should apply group tool policy overrides (group-specific beats wildcard)", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"*": {
|
||||
tools: { allow: ["read"] },
|
||||
},
|
||||
trusted: {
|
||||
tools: { allow: ["read", "exec"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const trustedTools = createClawdbotCodingTools({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:whatsapp:group:trusted",
|
||||
messageProvider: "whatsapp",
|
||||
workspaceDir: "/tmp/test-group-trusted",
|
||||
agentDir: "/tmp/agent-group",
|
||||
});
|
||||
const trustedNames = trustedTools.map((t) => t.name);
|
||||
expect(trustedNames).toContain("read");
|
||||
expect(trustedNames).toContain("exec");
|
||||
|
||||
const defaultTools = createClawdbotCodingTools({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:whatsapp:group:unknown",
|
||||
messageProvider: "whatsapp",
|
||||
workspaceDir: "/tmp/test-group-default",
|
||||
agentDir: "/tmp/agent-group",
|
||||
});
|
||||
const defaultNames = defaultTools.map((t) => t.name);
|
||||
expect(defaultNames).toContain("read");
|
||||
expect(defaultNames).not.toContain("exec");
|
||||
});
|
||||
|
||||
it("should resolve telegram group tool policy for topic session keys", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
groups: {
|
||||
"123": {
|
||||
tools: { allow: ["read"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tools = createClawdbotCodingTools({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:telegram:group:123:topic:456",
|
||||
messageProvider: "telegram",
|
||||
workspaceDir: "/tmp/test-telegram-topic",
|
||||
agentDir: "/tmp/agent-telegram",
|
||||
});
|
||||
const names = tools.map((t) => t.name);
|
||||
expect(names).toContain("read");
|
||||
expect(names).not.toContain("exec");
|
||||
});
|
||||
|
||||
it("should apply global tool policy before agent-specific policy", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { getChannelDock } from "../channels/dock.js";
|
||||
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
|
||||
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||
import type { SandboxToolPolicy } from "./sandbox.js";
|
||||
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js";
|
||||
|
||||
type CompiledPattern =
|
||||
| { kind: "all" }
|
||||
@@ -108,6 +112,23 @@ function normalizeProviderKey(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function resolveGroupContextFromSessionKey(sessionKey?: string | null): {
|
||||
channel?: string;
|
||||
groupId?: string;
|
||||
} {
|
||||
const raw = (sessionKey ?? "").trim();
|
||||
if (!raw) return {};
|
||||
const base = resolveThreadParentSessionKey(raw) ?? raw;
|
||||
const parts = base.split(":").filter(Boolean);
|
||||
const body = parts[0] === "agent" ? parts.slice(2) : parts;
|
||||
if (body.length < 3) return {};
|
||||
const [channel, kind, ...rest] = body;
|
||||
if (kind !== "group" && kind !== "channel") return {};
|
||||
const groupId = rest.join(":").trim();
|
||||
if (!groupId) return {};
|
||||
return { channel: channel.trim().toLowerCase(), groupId };
|
||||
}
|
||||
|
||||
function resolveProviderToolPolicy(params: {
|
||||
byProvider?: Record<string, ToolPolicyConfig>;
|
||||
modelProvider?: string;
|
||||
@@ -174,6 +195,45 @@ export function resolveEffectiveToolPolicy(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGroupToolPolicy(params: {
|
||||
config?: ClawdbotConfig;
|
||||
sessionKey?: string;
|
||||
messageProvider?: string;
|
||||
groupId?: string | null;
|
||||
groupChannel?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
}): SandboxToolPolicy | undefined {
|
||||
if (!params.config) return undefined;
|
||||
const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey);
|
||||
const groupId = params.groupId ?? sessionContext.groupId;
|
||||
if (!groupId) return undefined;
|
||||
const channelRaw = params.messageProvider ?? sessionContext.channel;
|
||||
const channel = normalizeMessageChannel(channelRaw);
|
||||
if (!channel) return undefined;
|
||||
let dock;
|
||||
try {
|
||||
dock = getChannelDock(channel);
|
||||
} catch {
|
||||
dock = undefined;
|
||||
}
|
||||
const toolsConfig =
|
||||
dock?.groups?.resolveToolPolicy?.({
|
||||
cfg: params.config,
|
||||
groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
accountId: params.accountId,
|
||||
}) ??
|
||||
resolveChannelGroupToolsPolicy({
|
||||
cfg: params.config,
|
||||
channel,
|
||||
groupId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
return pickToolPolicy(toolsConfig);
|
||||
}
|
||||
|
||||
export function isToolAllowedByPolicies(
|
||||
name: string,
|
||||
policies: Array<SandboxToolPolicy | undefined>,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
filterToolsByPolicy,
|
||||
isToolAllowedByPolicies,
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
resolveSubagentToolPolicy,
|
||||
} from "./pi-tools.policy.js";
|
||||
import {
|
||||
@@ -128,6 +129,12 @@ export function createClawdbotCodingTools(options?: {
|
||||
currentChannelId?: string;
|
||||
/** Current thread timestamp for auto-threading (Slack). */
|
||||
currentThreadTs?: string;
|
||||
/** Group id for channel-level tool policy resolution. */
|
||||
groupId?: string | null;
|
||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
||||
groupChannel?: string | null;
|
||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||
groupSpace?: string | null;
|
||||
/** Reply-to mode for Slack auto-threading. */
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||
@@ -151,6 +158,15 @@ export function createClawdbotCodingTools(options?: {
|
||||
modelProvider: options?.modelProvider,
|
||||
modelId: options?.modelId,
|
||||
});
|
||||
const groupPolicy = resolveGroupToolPolicy({
|
||||
config: options?.config,
|
||||
sessionKey: options?.sessionKey,
|
||||
messageProvider: options?.messageProvider,
|
||||
groupId: options?.groupId,
|
||||
groupChannel: options?.groupChannel,
|
||||
groupSpace: options?.groupSpace,
|
||||
accountId: options?.agentAccountId,
|
||||
});
|
||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
@@ -165,6 +181,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
sandbox?.tools,
|
||||
subagentPolicy,
|
||||
]);
|
||||
@@ -285,6 +302,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
sandbox?.tools,
|
||||
subagentPolicy,
|
||||
]),
|
||||
@@ -323,6 +341,10 @@ export function createClawdbotCodingTools(options?: {
|
||||
stripPluginOnlyAllowlist(agentProviderPolicy, pluginGroups),
|
||||
pluginGroups,
|
||||
);
|
||||
const groupPolicyExpanded = expandPolicyWithPluginGroups(
|
||||
stripPluginOnlyAllowlist(groupPolicy, pluginGroups),
|
||||
pluginGroups,
|
||||
);
|
||||
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
||||
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
||||
|
||||
@@ -344,9 +366,12 @@ export function createClawdbotCodingTools(options?: {
|
||||
const agentProviderFiltered = agentProviderExpanded
|
||||
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
||||
: agentFiltered;
|
||||
const sandboxed = sandboxPolicyExpanded
|
||||
? filterToolsByPolicy(agentProviderFiltered, sandboxPolicyExpanded)
|
||||
const groupFiltered = groupPolicyExpanded
|
||||
? filterToolsByPolicy(agentProviderFiltered, groupPolicyExpanded)
|
||||
: agentProviderFiltered;
|
||||
const sandboxed = sandboxPolicyExpanded
|
||||
? filterToolsByPolicy(groupFiltered, sandboxPolicyExpanded)
|
||||
: groupFiltered;
|
||||
const subagentFiltered = subagentPolicyExpanded
|
||||
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
||||
: sandboxed;
|
||||
|
||||
Reference in New Issue
Block a user