Merge branch 'pr-1757' into temp/landpr-1757

This commit is contained in:
Tyler Yust 2026-01-26 20:15:30 -08:00
commit 73c071d56d
26 changed files with 342 additions and 16 deletions

View File

@ -11,6 +11,7 @@ import type {
import { import {
buildChannelKeyCandidates, buildChannelKeyCandidates,
normalizeChannelSlug, normalizeChannelSlug,
resolveToolsBySender,
resolveChannelEntryMatchWithFallback, resolveChannelEntryMatchWithFallback,
resolveNestedAllowlistDecision, resolveNestedAllowlistDecision,
} from "clawdbot/plugin-sdk"; } from "clawdbot/plugin-sdk";
@ -106,9 +107,36 @@ export function resolveMSTeamsGroupToolPolicy(
}); });
if (resolved.channelConfig) { if (resolved.channelConfig) {
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools; const senderPolicy = resolveToolsBySender({
toolsBySender: resolved.channelConfig.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (senderPolicy) return senderPolicy;
if (resolved.channelConfig.tools) return resolved.channelConfig.tools;
const teamSenderPolicy = resolveToolsBySender({
toolsBySender: resolved.teamConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (teamSenderPolicy) return teamSenderPolicy;
return resolved.teamConfig?.tools;
}
if (resolved.teamConfig) {
const teamSenderPolicy = resolveToolsBySender({
toolsBySender: resolved.teamConfig.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (teamSenderPolicy) return teamSenderPolicy;
if (resolved.teamConfig.tools) return resolved.teamConfig.tools;
} }
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
if (!groupId) return undefined; if (!groupId) return undefined;
@ -125,7 +153,24 @@ export function resolveMSTeamsGroupToolPolicy(
normalizeKey: normalizeChannelSlug, normalizeKey: normalizeChannelSlug,
}); });
if (match.entry) { if (match.entry) {
return match.entry.tools ?? teamConfig?.tools; const senderPolicy = resolveToolsBySender({
toolsBySender: match.entry.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (senderPolicy) return senderPolicy;
if (match.entry.tools) return match.entry.tools;
const teamSenderPolicy = resolveToolsBySender({
toolsBySender: teamConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (teamSenderPolicy) return teamSenderPolicy;
return teamConfig?.tools;
} }
} }

View File

@ -3,6 +3,7 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import "./test-helpers/fast-coding-tools.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { ensureClawdbotModelsJson } from "./models-config.js"; import { ensureClawdbotModelsJson } from "./models-config.js";

View File

@ -215,6 +215,10 @@ export async function runEmbeddedAttempt(
groupChannel: params.groupChannel, groupChannel: params.groupChannel,
groupSpace: params.groupSpace, groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy, spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
sessionKey: params.sessionKey ?? params.sessionId, sessionKey: params.sessionKey ?? params.sessionId,
agentDir, agentDir,
workspaceDir: effectiveWorkspace, workspaceDir: effectiveWorkspace,

View File

@ -35,6 +35,10 @@ 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;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
/** Current channel ID for auto-threading (Slack). */ /** Current channel ID for auto-threading (Slack). */
currentChannelId?: string; currentChannelId?: string;
/** Current thread timestamp for auto-threading (Slack). */ /** Current thread timestamp for auto-threading (Slack). */

View File

@ -31,6 +31,10 @@ 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;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
currentChannelId?: string; currentChannelId?: string;
currentThreadTs?: string; currentThreadTs?: string;
replyToMode?: "off" | "first" | "all"; replyToMode?: "off" | "first" | "all";

View File

@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import "./test-helpers/fast-coding-tools.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { createClawdbotCodingTools } from "./pi-tools.js"; import { createClawdbotCodingTools } from "./pi-tools.js";
import type { SandboxDockerConfig } from "./sandbox.js"; import type { SandboxDockerConfig } from "./sandbox.js";
@ -270,6 +271,75 @@ describe("Agent-specific tool filtering", () => {
expect(defaultNames).not.toContain("exec"); expect(defaultNames).not.toContain("exec");
}); });
it("should apply per-sender tool policies for group tools", () => {
const cfg: ClawdbotConfig = {
channels: {
whatsapp: {
groups: {
"*": {
tools: { allow: ["read"] },
toolsBySender: {
alice: { allow: ["read", "exec"] },
},
},
},
},
},
};
const aliceTools = createClawdbotCodingTools({
config: cfg,
sessionKey: "agent:main:whatsapp:group:family",
senderId: "alice",
workspaceDir: "/tmp/test-group-sender",
agentDir: "/tmp/agent-group-sender",
});
const aliceNames = aliceTools.map((t) => t.name);
expect(aliceNames).toContain("read");
expect(aliceNames).toContain("exec");
const bobTools = createClawdbotCodingTools({
config: cfg,
sessionKey: "agent:main:whatsapp:group:family",
senderId: "bob",
workspaceDir: "/tmp/test-group-sender-bob",
agentDir: "/tmp/agent-group-sender",
});
const bobNames = bobTools.map((t) => t.name);
expect(bobNames).toContain("read");
expect(bobNames).not.toContain("exec");
});
it("should not let default sender policy override group tools", () => {
const cfg: ClawdbotConfig = {
channels: {
whatsapp: {
groups: {
"*": {
toolsBySender: {
admin: { allow: ["read", "exec"] },
},
},
locked: {
tools: { allow: ["read"] },
},
},
},
},
};
const adminTools = createClawdbotCodingTools({
config: cfg,
sessionKey: "agent:main:whatsapp:group:locked",
senderId: "admin",
workspaceDir: "/tmp/test-group-default-override",
agentDir: "/tmp/agent-group-default-override",
});
const adminNames = adminTools.map((t) => t.name);
expect(adminNames).toContain("read");
expect(adminNames).not.toContain("exec");
});
it("should resolve telegram group tool policy for topic session keys", () => { it("should resolve telegram group tool policy for topic session keys", () => {
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
channels: { channels: {

View File

@ -233,6 +233,10 @@ export function resolveGroupToolPolicy(params: {
groupChannel?: string | null; groupChannel?: string | null;
groupSpace?: string | null; groupSpace?: string | null;
accountId?: string | null; accountId?: string | null;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
}): SandboxToolPolicy | undefined { }): SandboxToolPolicy | undefined {
if (!params.config) return undefined; if (!params.config) return undefined;
const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey); const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey);
@ -255,12 +259,20 @@ export function resolveGroupToolPolicy(params: {
groupChannel: params.groupChannel, groupChannel: params.groupChannel,
groupSpace: params.groupSpace, groupSpace: params.groupSpace,
accountId: params.accountId, accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
}) ?? }) ??
resolveChannelGroupToolsPolicy({ resolveChannelGroupToolsPolicy({
cfg: params.config, cfg: params.config,
channel, channel,
groupId, groupId,
accountId: params.accountId, accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
}); });
return pickToolPolicy(toolsConfig); return pickToolPolicy(toolsConfig);
} }

View File

@ -140,6 +140,10 @@ export function createClawdbotCodingTools(options?: {
groupSpace?: string | null; groupSpace?: string | null;
/** Parent session key for subagent group policy inheritance. */ /** Parent session key for subagent group policy inheritance. */
spawnedBy?: string | null; spawnedBy?: string | null;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
/** Reply-to mode for Slack auto-threading. */ /** Reply-to mode for Slack auto-threading. */
replyToMode?: "off" | "first" | "all"; replyToMode?: "off" | "first" | "all";
/** Mutable ref to track if a reply was sent (for "first" mode). */ /** Mutable ref to track if a reply was sent (for "first" mode). */
@ -174,6 +178,10 @@ export function createClawdbotCodingTools(options?: {
groupChannel: options?.groupChannel, groupChannel: options?.groupChannel,
groupSpace: options?.groupSpace, groupSpace: options?.groupSpace,
accountId: options?.agentAccountId, accountId: options?.agentAccountId,
senderId: options?.senderId,
senderName: options?.senderName,
senderUsername: options?.senderUsername,
senderE164: options?.senderE164,
}); });
const profilePolicy = resolveToolProfilePolicy(profile); const profilePolicy = resolveToolProfilePolicy(profile);
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile); const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);

View File

@ -232,6 +232,10 @@ export async function runAgentTurnWithFallback(params: {
groupChannel: groupChannel:
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(), params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(),
groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined, groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined,
senderId: params.sessionCtx.SenderId?.trim() || undefined,
senderName: params.sessionCtx.SenderName?.trim() || undefined,
senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined,
senderE164: params.sessionCtx.SenderE164?.trim() || undefined,
// Provider threading context for tool auto-injection // Provider threading context for tool auto-injection
...buildThreadingToolContext({ ...buildThreadingToolContext({
sessionCtx: params.sessionCtx, sessionCtx: params.sessionCtx,

View File

@ -115,6 +115,10 @@ export async function runMemoryFlushIfNeeded(params: {
config: params.followupRun.run.config, config: params.followupRun.run.config,
hasRepliedRef: params.opts?.hasRepliedRef, hasRepliedRef: params.opts?.hasRepliedRef,
}), }),
senderId: params.sessionCtx.SenderId?.trim() || undefined,
senderName: params.sessionCtx.SenderName?.trim() || undefined,
senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined,
senderE164: params.sessionCtx.SenderE164?.trim() || undefined,
sessionFile: params.followupRun.run.sessionFile, sessionFile: params.followupRun.run.sessionFile,
workspaceDir: params.followupRun.run.workspaceDir, workspaceDir: params.followupRun.run.workspaceDir,
agentDir: params.followupRun.run.agentDir, agentDir: params.followupRun.run.agentDir,

View File

@ -147,6 +147,10 @@ export function createFollowupRunner(params: {
groupId: queued.run.groupId, groupId: queued.run.groupId,
groupChannel: queued.run.groupChannel, groupChannel: queued.run.groupChannel,
groupSpace: queued.run.groupSpace, groupSpace: queued.run.groupSpace,
senderId: queued.run.senderId,
senderName: queued.run.senderName,
senderUsername: queued.run.senderUsername,
senderE164: queued.run.senderE164,
sessionFile: queued.run.sessionFile, sessionFile: queued.run.sessionFile,
workspaceDir: queued.run.workspaceDir, workspaceDir: queued.run.workspaceDir,
config: queued.run.config, config: queued.run.config,

View File

@ -370,6 +370,10 @@ export async function runPreparedReply(
groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined, groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined,
groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(), groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(),
groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined, groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined,
senderId: sessionCtx.SenderId?.trim() || undefined,
senderName: sessionCtx.SenderName?.trim() || undefined,
senderUsername: sessionCtx.SenderUsername?.trim() || undefined,
senderE164: sessionCtx.SenderE164?.trim() || undefined,
sessionFile, sessionFile,
workspaceDir, workspaceDir,
config: cfg, config: cfg,

View File

@ -51,6 +51,10 @@ export type FollowupRun = {
groupId?: string; groupId?: string;
groupChannel?: string; groupChannel?: string;
groupSpace?: string; groupSpace?: string;
senderId?: string;
senderName?: string;
senderUsername?: string;
senderE164?: string;
sessionFile: string; sessionFile: string;
workspaceDir: string; workspaceDir: string;
config: ClawdbotConfig; config: ClawdbotConfig;

View File

@ -2,6 +2,7 @@ import type { ClawdbotConfig } from "../../config/config.js";
import { import {
resolveChannelGroupRequireMention, resolveChannelGroupRequireMention,
resolveChannelGroupToolsPolicy, resolveChannelGroupToolsPolicy,
resolveToolsBySender,
} from "../../config/group-policy.js"; } from "../../config/group-policy.js";
import type { DiscordConfig } from "../../config/types.js"; import type { DiscordConfig } from "../../config/types.js";
import type { GroupToolPolicyConfig } from "../../config/types.tools.js"; import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
@ -13,6 +14,10 @@ type GroupMentionParams = {
groupChannel?: string | null; groupChannel?: string | null;
groupSpace?: string | null; groupSpace?: string | null;
accountId?: string | null; accountId?: string | null;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
}; };
function normalizeDiscordSlug(value?: string | null) { function normalizeDiscordSlug(value?: string | null) {
@ -172,6 +177,10 @@ export function resolveGoogleChatGroupToolPolicy(
channel: "googlechat", channel: "googlechat",
groupId: params.groupId, groupId: params.groupId,
accountId: params.accountId, accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
}); });
} }
@ -226,6 +235,10 @@ export function resolveTelegramGroupToolPolicy(
channel: "telegram", channel: "telegram",
groupId: chatId ?? params.groupId, groupId: chatId ?? params.groupId,
accountId: params.accountId, accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
}); });
} }
@ -237,6 +250,10 @@ export function resolveWhatsAppGroupToolPolicy(
channel: "whatsapp", channel: "whatsapp",
groupId: params.groupId, groupId: params.groupId,
accountId: params.accountId, accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
}); });
} }
@ -248,6 +265,10 @@ export function resolveIMessageGroupToolPolicy(
channel: "imessage", channel: "imessage",
groupId: params.groupId, groupId: params.groupId,
accountId: params.accountId, accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
}); });
} }
@ -268,8 +289,24 @@ export function resolveDiscordGroupToolPolicy(
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`]) ? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
: undefined) ?? : undefined) ??
(groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined); (groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined);
const senderPolicy = resolveToolsBySender({
toolsBySender: entry?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (senderPolicy) return senderPolicy;
if (entry?.tools) return entry.tools; if (entry?.tools) return entry.tools;
} }
const guildSenderPolicy = resolveToolsBySender({
toolsBySender: guildEntry?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (guildSenderPolicy) return guildSenderPolicy;
if (guildEntry?.tools) return guildEntry.tools; if (guildEntry?.tools) return guildEntry.tools;
return undefined; return undefined;
} }
@ -302,6 +339,14 @@ export function resolveSlackGroupToolPolicy(
} }
} }
const resolved = matched ?? channels["*"]; const resolved = matched ?? channels["*"];
const senderPolicy = resolveToolsBySender({
toolsBySender: resolved?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (senderPolicy) return senderPolicy;
if (resolved?.tools) return resolved.tools; if (resolved?.tools) return resolved.tools;
return undefined; return undefined;
} }
@ -314,5 +359,9 @@ export function resolveBlueBubblesGroupToolPolicy(
channel: "bluebubbles", channel: "bluebubbles",
groupId: params.groupId, groupId: params.groupId,
accountId: params.accountId, accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
}); });
} }

View File

@ -155,6 +155,10 @@ export type ChannelGroupContext = {
groupChannel?: string | null; groupChannel?: string | null;
groupSpace?: string | null; groupSpace?: string | null;
accountId?: string | null; accountId?: string | null;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
}; };
export type ChannelCapabilities = { export type ChannelCapabilities = {

View File

@ -1,13 +1,14 @@
import type { ChannelId } from "../channels/plugins/types.js"; import type { ChannelId } from "../channels/plugins/types.js";
import { normalizeAccountId } from "../routing/session-key.js"; import { normalizeAccountId } from "../routing/session-key.js";
import type { ClawdbotConfig } from "./config.js"; import type { ClawdbotConfig } from "./config.js";
import type { GroupToolPolicyConfig } from "./types.tools.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type GroupPolicyChannel = ChannelId; export type GroupPolicyChannel = ChannelId;
export type ChannelGroupConfig = { export type ChannelGroupConfig = {
requireMention?: boolean; requireMention?: boolean;
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
}; };
export type ChannelGroupPolicy = { export type ChannelGroupPolicy = {
@ -19,6 +20,65 @@ export type ChannelGroupPolicy = {
type ChannelGroups = Record<string, ChannelGroupConfig>; type ChannelGroups = Record<string, ChannelGroupConfig>;
export type GroupToolPolicySender = {
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
};
function normalizeSenderKey(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
const withoutAt = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
return withoutAt.toLowerCase();
}
export function resolveToolsBySender(
params: {
toolsBySender?: GroupToolPolicyBySenderConfig;
} & GroupToolPolicySender,
): GroupToolPolicyConfig | undefined {
const toolsBySender = params.toolsBySender;
if (!toolsBySender) return undefined;
const entries = Object.entries(toolsBySender);
if (entries.length === 0) return undefined;
const normalized = new Map<string, GroupToolPolicyConfig>();
let wildcard: GroupToolPolicyConfig | undefined;
for (const [rawKey, policy] of entries) {
if (!policy) continue;
const key = normalizeSenderKey(rawKey);
if (!key) continue;
if (key === "*") {
wildcard = policy;
continue;
}
if (!normalized.has(key)) {
normalized.set(key, policy);
}
}
const candidates: string[] = [];
const pushCandidate = (value?: string | null) => {
const trimmed = value?.trim();
if (!trimmed) return;
candidates.push(trimmed);
};
pushCandidate(params.senderId);
pushCandidate(params.senderE164);
pushCandidate(params.senderUsername);
pushCandidate(params.senderName);
for (const candidate of candidates) {
const key = normalizeSenderKey(candidate);
if (!key) continue;
const match = normalized.get(key);
if (match) return match;
}
return wildcard;
}
function resolveChannelGroups( function resolveChannelGroups(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
channel: GroupPolicyChannel, channel: GroupPolicyChannel,
@ -94,14 +154,32 @@ export function resolveChannelGroupRequireMention(params: {
return true; return true;
} }
export function resolveChannelGroupToolsPolicy(params: { export function resolveChannelGroupToolsPolicy(
cfg: ClawdbotConfig; params: {
channel: GroupPolicyChannel; cfg: ClawdbotConfig;
groupId?: string | null; channel: GroupPolicyChannel;
accountId?: string | null; groupId?: string | null;
}): GroupToolPolicyConfig | undefined { accountId?: string | null;
} & GroupToolPolicySender,
): GroupToolPolicyConfig | undefined {
const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params); const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params);
const groupSenderPolicy = resolveToolsBySender({
toolsBySender: groupConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (groupSenderPolicy) return groupSenderPolicy;
if (groupConfig?.tools) return groupConfig.tools; if (groupConfig?.tools) return groupConfig.tools;
const defaultSenderPolicy = resolveToolsBySender({
toolsBySender: defaultConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (defaultSenderPolicy) return defaultSenderPolicy;
if (defaultConfig?.tools) return defaultConfig.tools; if (defaultConfig?.tools) return defaultConfig.tools;
return undefined; return undefined;
} }

View File

@ -8,7 +8,7 @@ import type {
} from "./types.base.js"; } from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js"; import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type DiscordDmConfig = { export type DiscordDmConfig = {
/** If false, ignore all incoming Discord DMs. Default: true. */ /** If false, ignore all incoming Discord DMs. Default: true. */
@ -28,6 +28,7 @@ export type DiscordGuildChannelConfig = {
requireMention?: boolean; requireMention?: boolean;
/** Optional tool policy overrides for this channel. */ /** Optional tool policy overrides for this channel. */
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
/** If specified, only load these skills for this channel. Omit = all skills; empty = no skills. */ /** If specified, only load these skills for this channel. Omit = all skills; empty = no skills. */
skills?: string[]; skills?: string[];
/** If false, disable the bot for this channel. */ /** If false, disable the bot for this channel. */
@ -45,6 +46,7 @@ export type DiscordGuildEntry = {
requireMention?: boolean; requireMention?: boolean;
/** Optional tool policy overrides for this guild (used when channel override is missing). */ /** Optional tool policy overrides for this guild (used when channel override is missing). */
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
/** Reaction notification mode (off|own|all|allowlist). Default: own. */ /** Reaction notification mode (off|own|all|allowlist). Default: own. */
reactionNotifications?: DiscordReactionNotificationMode; reactionNotifications?: DiscordReactionNotificationMode;
users?: Array<string | number>; users?: Array<string | number>;

View File

@ -6,7 +6,7 @@ import type {
} from "./types.base.js"; } from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js"; import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type IMessageAccountConfig = { export type IMessageAccountConfig = {
/** Optional display name for this account (used in CLI/UI lists). */ /** Optional display name for this account (used in CLI/UI lists). */
@ -64,6 +64,7 @@ export type IMessageAccountConfig = {
{ {
requireMention?: boolean; requireMention?: boolean;
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
} }
>; >;
/** Heartbeat visibility settings for this channel. */ /** Heartbeat visibility settings for this channel. */

View File

@ -6,7 +6,7 @@ import type {
} from "./types.base.js"; } from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js"; import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type MSTeamsWebhookConfig = { export type MSTeamsWebhookConfig = {
/** Port for the webhook server. Default: 3978. */ /** Port for the webhook server. Default: 3978. */
@ -24,6 +24,7 @@ export type MSTeamsChannelConfig = {
requireMention?: boolean; requireMention?: boolean;
/** Optional tool policy overrides for this channel. */ /** Optional tool policy overrides for this channel. */
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
/** Reply style: "thread" replies to the message, "top-level" posts a new message. */ /** Reply style: "thread" replies to the message, "top-level" posts a new message. */
replyStyle?: MSTeamsReplyStyle; replyStyle?: MSTeamsReplyStyle;
}; };
@ -34,6 +35,7 @@ export type MSTeamsTeamConfig = {
requireMention?: boolean; requireMention?: boolean;
/** Default tool policy for channels in this team. */ /** Default tool policy for channels in this team. */
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
/** Default reply style for channels in this team. */ /** Default reply style for channels in this team. */
replyStyle?: MSTeamsReplyStyle; replyStyle?: MSTeamsReplyStyle;
/** Per-channel overrides. Key is conversation ID (e.g., "19:...@thread.tacv2"). */ /** Per-channel overrides. Key is conversation ID (e.g., "19:...@thread.tacv2"). */

View File

@ -7,7 +7,7 @@ import type {
} from "./types.base.js"; } from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js"; import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type SlackDmConfig = { export type SlackDmConfig = {
/** If false, ignore all incoming Slack DMs. Default: true. */ /** If false, ignore all incoming Slack DMs. Default: true. */
@ -33,6 +33,7 @@ export type SlackChannelConfig = {
requireMention?: boolean; requireMention?: boolean;
/** Optional tool policy overrides for this channel. */ /** Optional tool policy overrides for this channel. */
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
/** Allow bot-authored messages to trigger replies (default: false). */ /** Allow bot-authored messages to trigger replies (default: false). */
allowBots?: boolean; allowBots?: boolean;
/** Allowlist of users that can invoke the bot in this channel. */ /** Allowlist of users that can invoke the bot in this channel. */

View File

@ -9,7 +9,7 @@ import type {
} from "./types.base.js"; } from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js"; import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type TelegramActionConfig = { export type TelegramActionConfig = {
reactions?: boolean; reactions?: boolean;
@ -146,6 +146,7 @@ export type TelegramGroupConfig = {
requireMention?: boolean; requireMention?: boolean;
/** Optional tool policy overrides for this group. */ /** Optional tool policy overrides for this group. */
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
/** If specified, only load these skills for this group (when no topic). Omit = all skills; empty = no skills. */ /** If specified, only load these skills for this group (when no topic). Omit = all skills; empty = no skills. */
skills?: string[]; skills?: string[];
/** Per-topic configuration (key is message_thread_id as string) */ /** Per-topic configuration (key is message_thread_id as string) */

View File

@ -158,6 +158,8 @@ export type GroupToolPolicyConfig = {
deny?: string[]; deny?: string[];
}; };
export type GroupToolPolicyBySenderConfig = Record<string, GroupToolPolicyConfig>;
export type ExecToolConfig = { export type ExecToolConfig = {
/** Exec host routing (default: sandbox). */ /** Exec host routing (default: sandbox). */
host?: "sandbox" | "gateway" | "node"; host?: "sandbox" | "gateway" | "node";

View File

@ -6,7 +6,7 @@ import type {
} from "./types.base.js"; } from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js"; import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type WhatsAppActionConfig = { export type WhatsAppActionConfig = {
reactions?: boolean; reactions?: boolean;
@ -70,6 +70,7 @@ export type WhatsAppConfig = {
{ {
requireMention?: boolean; requireMention?: boolean;
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
} }
>; >;
/** Acknowledgment reaction sent immediately upon message receipt. */ /** Acknowledgment reaction sent immediately upon message receipt. */
@ -135,6 +136,7 @@ export type WhatsAppAccountConfig = {
{ {
requireMention?: boolean; requireMention?: boolean;
tools?: GroupToolPolicyConfig; tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
} }
>; >;
/** Acknowledgment reaction sent immediately upon message receipt. */ /** Acknowledgment reaction sent immediately upon message receipt. */

View File

@ -22,6 +22,8 @@ import {
resolveTelegramCustomCommands, resolveTelegramCustomCommands,
} from "./telegram-custom-commands.js"; } from "./telegram-custom-commands.js";
const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional();
const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "allowlist"]); const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "allowlist"]);
const TelegramCapabilitiesSchema = z.union([ const TelegramCapabilitiesSchema = z.union([
@ -47,6 +49,7 @@ export const TelegramGroupSchema = z
.object({ .object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
skills: z.array(z.string()).optional(), skills: z.array(z.string()).optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
@ -186,6 +189,7 @@ export const DiscordGuildChannelSchema = z
allow: z.boolean().optional(), allow: z.boolean().optional(),
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
skills: z.array(z.string()).optional(), skills: z.array(z.string()).optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
users: z.array(z.union([z.string(), z.number()])).optional(), users: z.array(z.union([z.string(), z.number()])).optional(),
@ -199,6 +203,7 @@ export const DiscordGuildSchema = z
slug: z.string().optional(), slug: z.string().optional(),
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(), reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
users: z.array(z.union([z.string(), z.number()])).optional(), users: z.array(z.union([z.string(), z.number()])).optional(),
channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(), channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(),
@ -374,6 +379,7 @@ export const SlackChannelSchema = z
allow: z.boolean().optional(), allow: z.boolean().optional(),
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
allowBots: z.boolean().optional(), allowBots: z.boolean().optional(),
users: z.array(z.union([z.string(), z.number()])).optional(), users: z.array(z.union([z.string(), z.number()])).optional(),
skills: z.array(z.string()).optional(), skills: z.array(z.string()).optional(),
@ -584,6 +590,7 @@ export const IMessageAccountSchemaBase = z
.object({ .object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
}) })
.strict() .strict()
.optional(), .optional(),
@ -640,6 +647,7 @@ const BlueBubblesGroupConfigSchema = z
.object({ .object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
}) })
.strict(); .strict();
@ -699,6 +707,7 @@ export const MSTeamsChannelSchema = z
.object({ .object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
replyStyle: MSTeamsReplyStyleSchema.optional(), replyStyle: MSTeamsReplyStyleSchema.optional(),
}) })
.strict(); .strict();
@ -707,6 +716,7 @@ export const MSTeamsTeamSchema = z
.object({ .object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
replyStyle: MSTeamsReplyStyleSchema.optional(), replyStyle: MSTeamsReplyStyleSchema.optional(),
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(), channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
}) })

View File

@ -10,6 +10,8 @@ import {
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js"; import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js"; import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional();
export const WhatsAppAccountSchema = z export const WhatsAppAccountSchema = z
.object({ .object({
name: z.string().optional(), name: z.string().optional(),
@ -41,6 +43,7 @@ export const WhatsAppAccountSchema = z
.object({ .object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
}) })
.strict() .strict()
.optional(), .optional(),
@ -105,6 +108,7 @@ export const WhatsAppConfigSchema = z
.object({ .object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
tools: ToolPolicySchema, tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
}) })
.strict() .strict()
.optional(), .optional(),

View File

@ -81,6 +81,7 @@ export type {
DmConfig, DmConfig,
GroupPolicy, GroupPolicy,
GroupToolPolicyConfig, GroupToolPolicyConfig,
GroupToolPolicyBySenderConfig,
MarkdownConfig, MarkdownConfig,
MarkdownTableMode, MarkdownTableMode,
GoogleChatAccountConfig, GoogleChatAccountConfig,
@ -121,6 +122,7 @@ export { resolveAckReaction } from "../agents/identity.js";
export type { ReplyPayload } from "../auto-reply/types.js"; export type { ReplyPayload } from "../auto-reply/types.js";
export type { ChunkMode } from "../auto-reply/chunk.js"; export type { ChunkMode } from "../auto-reply/chunk.js";
export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js"; export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js";
export { resolveToolsBySender } from "../config/group-policy.js";
export { export {
buildPendingHistoryContextFromMap, buildPendingHistoryContextFromMap,
clearHistoryEntries, clearHistoryEntries,