diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index ad4fa7853..22c00874f 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -78,5 +78,6 @@ export type AgentBinding = { peer?: { kind: ChatType; id: string }; guildId?: string; teamId?: string; + roles?: string[]; }; }; diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index 393544689..5f056d1bc 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -36,6 +36,8 @@ export type DiscordGuildChannelConfig = { enabled?: boolean; /** Optional allowlist for channel senders (ids or names). */ users?: Array; + /** Optional allowlist for channel senders by role (ids or names). */ + roles?: Array; /** Optional system prompt snippet for this channel. */ systemPrompt?: string; /** If false, omit thread starter context for this channel (default: true). */ @@ -53,6 +55,7 @@ export type DiscordGuildEntry = { /** Reaction notification mode (off|own|all|allowlist). Default: own. */ reactionNotifications?: DiscordReactionNotificationMode; users?: Array; + roles?: Array; channels?: Record; }; diff --git a/src/config/zod-schema.agents.ts b/src/config/zod-schema.agents.ts index 92947c2a8..704d1752c 100644 --- a/src/config/zod-schema.agents.ts +++ b/src/config/zod-schema.agents.ts @@ -35,6 +35,7 @@ export const BindingsSchema = z .optional(), guildId: z.string().optional(), teamId: z.string().optional(), + roles: z.array(z.string()).optional(), }) .strict(), }) diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 9c4fc422a..447ea5aca 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -235,6 +235,7 @@ export const DiscordGuildChannelSchema = z skills: z.array(z.string()).optional(), enabled: z.boolean().optional(), users: z.array(z.union([z.string(), z.number()])).optional(), + roles: z.array(z.union([z.string(), z.number()])).optional(), systemPrompt: z.string().optional(), includeThreadStarter: z.boolean().optional(), autoThread: z.boolean().optional(), @@ -249,6 +250,7 @@ export const DiscordGuildSchema = z toolsBySender: ToolPolicyBySenderSchema, reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(), users: z.array(z.union([z.string(), z.number()])).optional(), + roles: z.array(z.union([z.string(), z.number()])).optional(), channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(), }) .strict(); diff --git a/src/discord/monitor/allow-list.ts b/src/discord/monitor/allow-list.ts index e95937601..0d792673f 100644 --- a/src/discord/monitor/allow-list.ts +++ b/src/discord/monitor/allow-list.ts @@ -22,6 +22,7 @@ export type DiscordGuildEntryResolved = { requireMention?: boolean; reactionNotifications?: "off" | "own" | "all" | "allowlist"; users?: Array; + roles?: Array; channels?: Record< string, { @@ -30,6 +31,7 @@ export type DiscordGuildEntryResolved = { skills?: string[]; enabled?: boolean; users?: Array; + roles?: Array; systemPrompt?: string; includeThreadStarter?: boolean; autoThread?: boolean; @@ -43,6 +45,7 @@ export type DiscordChannelConfigResolved = { skills?: string[]; enabled?: boolean; users?: Array; + roles?: Array; systemPrompt?: string; includeThreadStarter?: boolean; autoThread?: boolean; @@ -283,6 +286,7 @@ function resolveDiscordChannelConfigEntry( skills: entry.skills, enabled: entry.enabled, users: entry.users, + roles: entry.roles, systemPrompt: entry.systemPrompt, includeThreadStarter: entry.includeThreadStarter, autoThread: entry.autoThread, diff --git a/src/routing/resolve-route.ts b/src/routing/resolve-route.ts index 381a90f01..02f5f0c77 100644 --- a/src/routing/resolve-route.ts +++ b/src/routing/resolve-route.ts @@ -29,6 +29,8 @@ export type ResolveAgentRouteInput = { parentPeer?: RoutePeer | null; guildId?: string | null; teamId?: string | null; + /** Discord member role IDs — used for role-based agent routing. */ + memberRoleIds?: string[]; }; export type ResolvedAgentRoute = { @@ -169,12 +171,24 @@ function matchesTeam(match: { teamId?: string | undefined } | undefined, teamId: return id === teamId; } +function matchesRoles( + match: { roles?: string[] | undefined } | undefined, + memberRoleIds: string[], +): boolean { + const roles = match?.roles; + if (!Array.isArray(roles) || roles.length === 0) { + return false; + } + return roles.some((r) => memberRoleIds.includes(r)); +} + export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentRoute { const channel = normalizeToken(input.channel); const accountId = normalizeAccountId(input.accountId); const peer = input.peer ? { kind: input.peer.kind, id: normalizeId(input.peer.id) } : null; const guildId = normalizeId(input.guildId); const teamId = normalizeId(input.teamId); + const memberRoleIds = input.memberRoleIds ?? []; const bindings = listBindings(input.cfg).filter((binding) => { if (!binding || typeof binding !== "object") {