diff --git a/.changeset/twenty-deserts-spend.md b/.changeset/twenty-deserts-spend.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/twenty-deserts-spend.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/scripts/test-tool-sets-local.ts b/server/scripts/test-tool-sets-local.ts index 45856c5502..14fb64503a 100644 --- a/server/scripts/test-tool-sets-local.ts +++ b/server/scripts/test-tool-sets-local.ts @@ -168,8 +168,8 @@ const TEST_CASES = [ /** * Build the router prompt */ -function buildRouterPrompt(message: string, isAdmin = false): string { - const toolSetsSection = getToolSetDescriptionsForRouter(isAdmin); +function buildRouterPrompt(message: string, isAAOAdmin = false): string { + const toolSetsSection = getToolSetDescriptionsForRouter(isAAOAdmin); return `You are Addie's router. Analyze this message and select the appropriate tool SETS. @@ -195,7 +195,7 @@ IMPORTANT: Select tool SETS based on the user's INTENT: ## Instructions Respond with a JSON object: {"tool_sets": ["set1", "set2"], "reason": "brief reason"} -Valid sets: knowledge, member, directory, agent_testing, adcp_operations, content, billing, meetings${isAdmin ? ', admin' : ''} +Valid sets: knowledge, member, directory, agent_testing, adcp_operations, content, billing, meetings${isAAOAdmin ? ', admin' : ''} Empty array [] means respond without tools (general knowledge) Respond with ONLY the JSON object, no other text.`; diff --git a/server/scripts/test-tool-sets.ts b/server/scripts/test-tool-sets.ts index a10c253eaa..e263d7c502 100644 --- a/server/scripts/test-tool-sets.ts +++ b/server/scripts/test-tool-sets.ts @@ -74,8 +74,8 @@ async function fetchTestMessages(limit = 50): Promise { /** * Build the new router prompt for tool set selection */ -function buildToolSetRouterPrompt(message: string, isAdmin = false): string { - const toolSetsSection = getToolSetDescriptionsForRouter(isAdmin); +function buildToolSetRouterPrompt(message: string, isAAOAdmin = false): string { + const toolSetsSection = getToolSetDescriptionsForRouter(isAAOAdmin); return `You are Addie's router. Analyze this message and select the appropriate tool SETS. @@ -101,7 +101,7 @@ IMPORTANT: Select tool SETS based on the user's INTENT: ## Instructions Respond with a JSON object: {"tool_sets": ["set1", "set2"], "reason": "brief reason"} -Valid sets: knowledge, member, directory, agent_testing, adcp_operations, content, billing, meetings${isAdmin ? ', admin' : ''} +Valid sets: knowledge, member, directory, agent_testing, adcp_operations, content, billing, meetings${isAAOAdmin ? ', admin' : ''} Empty array [] means respond without tools (general knowledge) Respond with ONLY the JSON object, no other text.`; diff --git a/server/src/addie/bolt-app.ts b/server/src/addie/bolt-app.ts index 0533b25f49..762611320f 100644 --- a/server/src/addie/bolt-app.ts +++ b/server/src/addie/bolt-app.ts @@ -44,7 +44,7 @@ import { import { ADMIN_TOOLS, createAdminToolHandlers, - isSlackUserAdmin, + isSlackUserAAOAdmin, } from './mcp/admin-tools.js'; import { EVENT_TOOLS, @@ -606,7 +606,7 @@ export function invalidateAddieRulesCache(): void { async function getDynamicSuggestedPrompts(userId: string): Promise { try { const memberContext = await getMemberContext(userId); - const userIsAdmin = await isSlackUserAdmin(userId); + const userIsAdmin = await isSlackUserAAOAdmin(userId); return buildDynamicSuggestedPrompts(memberContext, userIsAdmin); } catch (error) { logger.warn({ error, userId }, 'Addie Bolt: Failed to build dynamic prompts, using defaults'); @@ -714,7 +714,7 @@ async function createUserScopedTools( logger.debug('Addie Bolt: AdCP protocol tools enabled'); // Check if user is AAO admin (based on aao-admin working group membership) - const userIsAdmin = slackUserId ? await isSlackUserAdmin(slackUserId) : false; + const userIsAdmin = slackUserId ? await isSlackUserAAOAdmin(slackUserId) : false; // Add admin tools if user is admin if (userIsAdmin) { @@ -782,7 +782,7 @@ async function createUserScopedTools( tools: allTools, handlers: allHandlers, }, - isAdmin: userIsAdmin, + isAAOAdmin: userIsAdmin, }; } @@ -792,16 +792,16 @@ async function createUserScopedTools( * * @param userTools - All tools available to the user * @param selectedSets - Tool set names from the router's execution plan - * @param isAdmin - Whether the user is an admin (affects which sets are valid) + * @param isAAOAdmin - Whether the user is an AAO admin (affects which sets are valid) * @returns Filtered tools and a hint about unavailable sets */ function filterToolsBySet( userTools: RequestTools, selectedSets: string[], - isAdmin: boolean + isAAOAdmin: boolean ): { filteredTools: RequestTools; unavailableHint: string } { // Get all tool names that should be available based on selected sets - const allowedToolNames = new Set(getToolsForSets(selectedSets, isAdmin)); + const allowedToolNames = new Set(getToolsForSets(selectedSets, isAAOAdmin)); // Filter tools to only those allowed const filteredToolDefs = userTools.tools.filter(tool => allowedToolNames.has(tool.name)); @@ -815,7 +815,7 @@ function filterToolsBySet( } // Build hint about unavailable tool sets - const unavailableHint = buildUnavailableSetsHint(selectedSets, isAdmin); + const unavailableHint = buildUnavailableSetsHint(selectedSets, isAAOAdmin); logger.debug({ selectedSets, @@ -1111,7 +1111,7 @@ async function handleUserMessage({ }); // Create user-scoped tools (includes admin tools if user is admin, meeting tools with channel context) - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, slackThreadContext); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, slackThreadContext); // Admin users get higher iteration limit for bulk operations const processOptions = userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : undefined; @@ -1536,7 +1536,7 @@ async function handleAppMention({ }); // Create user-scoped tools (includes admin tools if user is admin, meeting tools with channel context) - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, mentionChannelContext); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, mentionChannelContext); // Admin users get higher iteration limit for bulk operations const processOptions = userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : undefined; @@ -2000,7 +2000,7 @@ async function handleDirectMessage( }); // Create user-scoped tools - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id); // Admin users get higher iteration limit for bulk operations const processOptions = userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : undefined; @@ -2292,7 +2292,7 @@ async function handleActiveThreadReply({ }); // Create user-scoped tools (pass channel context for working group auto-detection) - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, channelContext); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, channelContext); // Admin users get higher iteration limit const processOptions = userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : undefined; @@ -2564,11 +2564,12 @@ async function handleChannelMessage({ logger.debug({ error, channelId }, 'Addie Bolt: Could not get channel context'); } - // Fetch member context and insights in parallel (both are independent) + // Fetch member context, insights, and admin status in parallel (all independent) // Insights use a cache with 5-minute TTL to reduce DB load - const [memberContext, memberInsights] = await Promise.all([ + const [memberContext, memberInsights, isAdminForRouting] = await Promise.all([ getMemberContext(userId), getCachedInsights(userId), + isSlackUserAAOAdmin(userId), ]); if (memberInsights && memberInsights.length > 0) { @@ -2585,6 +2586,7 @@ async function handleChannelMessage({ memberContext, isThread: isInThread, memberInsights, + isAAOAdmin: isAdminForRouting, }; // Quick match first (no API call for obvious cases) @@ -2691,7 +2693,7 @@ async function handleChannelMessage({ ); // Get all user-scoped tools then filter by selected tool sets - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, channelContext); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, userId, thread.thread_id, channelContext); const { filteredTools, unavailableHint } = filterToolsBySet(userTools, plan.tool_sets, userIsAdmin); // Build SI context from retrieved agents @@ -2996,7 +2998,7 @@ async function handleViewFlagged({ ack, body, client }: any): Promise { logger.debug({ userId }, 'Addie Bolt: View Flagged clicked'); // Verify admin status - const admin = await isSlackUserAdmin(userId); + const admin = await isSlackUserAAOAdmin(userId); if (!admin) { logger.warn({ userId }, 'Addie Bolt: Non-admin tried to view flagged threads'); return; @@ -3176,7 +3178,7 @@ async function handleReactionAdded({ ); // Create user-scoped tools (pass channel context for working group auto-detection) - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, reactingUserId, thread.thread_id, channelContext); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, reactingUserId, thread.thread_id, channelContext); // Admin users get higher iteration limit for bulk operations const processOptions = userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : undefined; diff --git a/server/src/addie/claude-client.ts b/server/src/addie/claude-client.ts index 4b5e3da508..ab6c673346 100644 --- a/server/src/addie/claude-client.ts +++ b/server/src/addie/claude-client.ts @@ -93,7 +93,7 @@ export interface RequestTools { */ export interface UserScopedToolsResult { tools: RequestTools; - isAdmin: boolean; + isAAOAdmin: boolean; } /** diff --git a/server/src/addie/email-handler.ts b/server/src/addie/email-handler.ts index 0f78cb4e6d..70f2df58ba 100644 --- a/server/src/addie/email-handler.ts +++ b/server/src/addie/email-handler.ts @@ -13,7 +13,7 @@ import { generateInteractionId, } from './security.js'; import { getWebMemberContext, formatMemberContextForPrompt, type MemberContext } from './member-context.js'; -import { isWebUserAdmin, ADMIN_TOOLS, createAdminToolHandlers } from './mcp/admin-tools.js'; +import { isWebUserAAOAdmin, ADMIN_TOOLS, createAdminToolHandlers } from './mcp/admin-tools.js'; import { MEMBER_TOOLS, createMemberToolHandlers } from './mcp/member-tools.js'; import { BILLING_TOOLS, createBillingToolHandlers } from './mcp/billing-tools.js'; import { sendEmailReply, type EmailThreadContext } from '../notifications/email.js'; @@ -261,7 +261,7 @@ export async function handleEmailInvocation( if (senderWorkosUserId) { memberContext = await getWebMemberContext(senderWorkosUserId); // Check if user is AAO admin (based on aao-admin working group membership) - isUserAdmin = await isWebUserAdmin(senderWorkosUserId); + isUserAdmin = await isWebUserAAOAdmin(senderWorkosUserId); } // Sanitize input diff --git a/server/src/addie/handler.ts b/server/src/addie/handler.ts index 4d471052e4..40cdfb46f2 100644 --- a/server/src/addie/handler.ts +++ b/server/src/addie/handler.ts @@ -30,7 +30,7 @@ import { import { ADMIN_TOOLS, createAdminToolHandlers, - isSlackUserAdmin, + isSlackUserAAOAdmin, } from './mcp/admin-tools.js'; import { MEMBER_TOOLS, @@ -330,7 +330,7 @@ async function createUserScopedTools( } // Check if user is AAO admin (based on aao-admin working group membership) - const userIsAdmin = slackUserId ? await isSlackUserAdmin(slackUserId) : false; + const userIsAdmin = slackUserId ? await isSlackUserAAOAdmin(slackUserId) : false; // Add admin tools if user is admin if (userIsAdmin) { @@ -398,7 +398,7 @@ async function createUserScopedTools( tools: allTools, handlers: allHandlers, }, - isAdmin: userIsAdmin, + isAAOAdmin: userIsAdmin, }; } @@ -408,7 +408,7 @@ async function createUserScopedTools( async function getDynamicSuggestedPrompts(userId: string): Promise { try { const memberContext = await getMemberContext(userId); - const userIsAdmin = await isSlackUserAdmin(userId); + const userIsAdmin = await isSlackUserAAOAdmin(userId); return buildDynamicSuggestedPrompts(memberContext, userIsAdmin); } catch (error) { logger.warn({ error, userId }, 'Addie: Failed to build dynamic prompts, using defaults'); @@ -462,9 +462,9 @@ export async function handleAssistantMessage( const startTime = Date.now(); const interactionId = generateInteractionId(); - // Check if user is an admin (for admin-only tools access) - const isAdmin = await isSlackUserAdmin(event.user); - logger.debug({ userId: event.user, isAdmin }, 'Addie: Checked admin status'); + // Check if user is an AAO admin (for admin-only tools access) + const isAAOAdmin = await isSlackUserAAOAdmin(event.user); + logger.debug({ userId: event.user, isAAOAdmin }, 'Addie: Checked admin status'); // Resolve user mentions to include names (e.g., <@U123> -> <@U123|John>) const textWithResolvedMentions = await resolveSlackMentions(event.text, lookupSlackUserName); @@ -483,7 +483,7 @@ export async function handleAssistantMessage( const { message: messageWithContext, memberContext } = await buildMessageWithMemberContext( event.user, inputValidation.sanitized, - isAdmin + isAAOAdmin ); // Check for sensitive topics before processing @@ -512,7 +512,7 @@ export async function handleAssistantMessage( }; } else { // Create user-scoped tools (these can only operate on behalf of this user) - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, event.user, event.thread_ts); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, event.user, event.thread_ts); // Admin users get higher iteration limit for bulk operations const processOptions = userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : undefined; @@ -616,9 +616,9 @@ export async function handleAppMention(event: AppMentionEvent): Promise { const startTime = Date.now(); const interactionId = generateInteractionId(); - // Check if user is an admin (for admin-only tools access) - const isAdmin = await isSlackUserAdmin(event.user); - logger.debug({ userId: event.user, isAdmin }, 'Addie: Checked admin status for mention'); + // Check if user is an AAO admin (for admin-only tools access) + const isAAOAdmin = await isSlackUserAAOAdmin(event.user); + logger.debug({ userId: event.user, isAAOAdmin }, 'Addie: Checked admin status for mention'); // Strip bot mention const rawText = botUserId ? stripBotMention(event.text, botUserId) : event.text; @@ -633,7 +633,7 @@ export async function handleAppMention(event: AppMentionEvent): Promise { const { message: messageWithContext, memberContext } = await buildMessageWithMemberContext( event.user, inputValidation.sanitized, - isAdmin + isAAOAdmin ); // Check for sensitive topics before processing (channel mentions are more public) @@ -663,7 +663,7 @@ export async function handleAppMention(event: AppMentionEvent): Promise { }; } else { // Create user-scoped tools (these can only operate on behalf of this user) - const { tools: userTools, isAdmin: userIsAdmin } = await createUserScopedTools(memberContext, event.user, event.thread_ts || event.ts); + const { tools: userTools, isAAOAdmin: userIsAdmin } = await createUserScopedTools(memberContext, event.user, event.thread_ts || event.ts); // Admin users get higher iteration limit for bulk operations const processOptions = userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : undefined; diff --git a/server/src/addie/home/builders/quick-actions.ts b/server/src/addie/home/builders/quick-actions.ts index 6bbd8458c4..e4b8fd0b97 100644 --- a/server/src/addie/home/builders/quick-actions.ts +++ b/server/src/addie/home/builders/quick-actions.ts @@ -13,7 +13,7 @@ import type { MemberContext } from '../../member-context.js'; */ export function buildQuickActions( memberContext: MemberContext, - isAdmin: boolean + isAAOAdmin: boolean ): QuickAction[] { const actions: QuickAction[] = []; @@ -45,7 +45,7 @@ export function buildQuickActions( }); // Admin: View Flagged Threads - if (isAdmin) { + if (isAAOAdmin) { actions.push({ id: 'view-flagged', label: 'Flagged Threads', diff --git a/server/src/addie/home/home-service.ts b/server/src/addie/home/home-service.ts index d0c3c5d69d..d7b756c348 100644 --- a/server/src/addie/home/home-service.ts +++ b/server/src/addie/home/home-service.ts @@ -8,7 +8,7 @@ import type { HomeContent, GreetingSection } from './types.js'; import { getHomeContentCache } from './cache.js'; import { getMemberContext, type MemberContext } from '../member-context.js'; -import { isSlackUserAdmin } from '../mcp/admin-tools.js'; +import { isSlackUserAAOAdmin } from '../mcp/admin-tools.js'; import { buildAlerts } from './builders/alerts.js'; import { buildQuickActions } from './builders/quick-actions.js'; import { buildActivityFeed } from './builders/activity.js'; @@ -45,21 +45,21 @@ export async function getHomeContent( const memberContext = await getMemberContext(slackUserId); // Check if user is admin - const isAdmin = await isSlackUserAdmin(slackUserId); + const isAAOAdmin = await isSlackUserAAOAdmin(slackUserId); // Get admin user ID for prospect stats (if admin) - const adminUserId = isAdmin ? memberContext?.workos_user?.workos_user_id : undefined; + const adminUserId = isAAOAdmin ? memberContext?.workos_user?.workos_user_id : undefined; // Build all sections in parallel for speed const [alerts, activity, adminPanel] = await Promise.all([ buildAlerts(memberContext), buildActivityFeed(memberContext), - isAdmin ? buildAdminPanel(adminUserId) : Promise.resolve(null), + isAAOAdmin ? buildAdminPanel(adminUserId) : Promise.resolve(null), ]); // Build synchronous sections const greeting = buildGreeting(memberContext); - const quickActions = buildQuickActions(memberContext, isAdmin); + const quickActions = buildQuickActions(memberContext, isAAOAdmin); const stats = buildStats(memberContext); const content: HomeContent = { @@ -79,7 +79,7 @@ export async function getHomeContent( { slackUserId, isMember: memberContext.is_member, - isAdmin, + isAAOAdmin, alertCount: alerts.length, activityCount: activity.length, }, diff --git a/server/src/addie/home/web-home-service.ts b/server/src/addie/home/web-home-service.ts index 2d90122ea7..edf319dd2c 100644 --- a/server/src/addie/home/web-home-service.ts +++ b/server/src/addie/home/web-home-service.ts @@ -7,7 +7,7 @@ import type { HomeContent, GreetingSection } from './types.js'; import { getWebMemberContext, type MemberContext } from '../member-context.js'; -import { isWebUserAdmin } from '../mcp/admin-tools.js'; +import { isWebUserAAOAdmin } from '../mcp/admin-tools.js'; import { buildAlerts } from './builders/alerts.js'; import { buildQuickActions } from './builders/quick-actions.js'; import { buildActivityFeed } from './builders/activity.js'; @@ -25,7 +25,7 @@ export async function getWebHomeContent(workosUserId: string): Promise { +export async function isSlackUserAAOAdmin(slackUserId: string): Promise { // Check cache first const cached = adminStatusCache.get(slackUserId); if (cached && cached.expiresAt > Date.now()) { @@ -174,7 +174,7 @@ export function invalidateAllAdminCaches(): void { * Checks membership in aao-admin working group by WorkOS user ID * Results are cached for 30 minutes to reduce DB load */ -export async function isWebUserAdmin(workosUserId: string): Promise { +export async function isWebUserAAOAdmin(workosUserId: string): Promise { // Check cache first const cached = webAdminStatusCache.get(workosUserId); if (cached && cached.expiresAt > Date.now()) { @@ -206,13 +206,6 @@ export async function isWebUserAdmin(workosUserId: string): Promise { } } -/** - * Check if a web user has admin privileges (via member context org role) - * @deprecated Use isWebUserAdmin() for AAO admin checks - this only checks WorkOS org role - */ -export function isAdmin(memberContext: MemberContext | null): boolean { - return memberContext?.org_membership?.role === 'admin'; -} /** * Compute the unified lifecycle stage for an organization. @@ -1146,21 +1139,14 @@ function formatOpenInvoice(invoice: OpenInvoiceWithCustomer): Record) => Promise> { const handlers = new Map) => Promise>(); - // Helper for prospect tools that need member context for admin check - const requireAdminFromContext = (): string | null => { - if (memberContext && !isAdmin(memberContext)) { - return '⚠️ This tool requires admin access. If you believe you should have admin access, please contact your organization administrator.'; - } - return null; - }; // ============================================ // BILLING & INVOICE HANDLERS @@ -1168,8 +1154,6 @@ export function createAdminToolHandlers( // List pending invoices across all customers (queries Stripe directly) handlers.set('list_pending_invoices', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const limit = (input.limit as number) || 20; @@ -1232,8 +1216,6 @@ export function createAdminToolHandlers( // Shared handler for get_account and get_organization_details const getAccountHandler = async (input: Record) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const query = input.query as string; @@ -1516,8 +1498,6 @@ export function createAdminToolHandlers( // Add prospect handlers.set('add_prospect', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const name = input.name as string; const companyType = (input.company_type as string) || undefined; @@ -1589,8 +1569,6 @@ export function createAdminToolHandlers( // Update prospect handlers.set('update_prospect', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const orgId = input.org_id as string; @@ -1682,8 +1660,6 @@ export function createAdminToolHandlers( // Enrich company handlers.set('enrich_company', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; if (!isLushaConfigured()) { return '❌ Enrichment is not configured (LUSHA_API_KEY not set).'; @@ -1761,8 +1737,6 @@ export function createAdminToolHandlers( // List prospects handlers.set('list_prospects', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const status = input.status as string | undefined; @@ -1838,8 +1812,6 @@ export function createAdminToolHandlers( // Send payment request - the unified tool for getting prospects to pay handlers.set('send_payment_request', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const companyName = input.company_name as string; const domain = input.domain as string | undefined; @@ -2424,8 +2396,6 @@ export function createAdminToolHandlers( // Search Lusha for prospects handlers.set('prospect_search_lusha', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; if (!isLushaConfigured()) { return '❌ Lusha is not configured (LUSHA_API_KEY not set).'; @@ -2478,8 +2448,6 @@ export function createAdminToolHandlers( // Search industry feeds handlers.set('search_industry_feeds', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const query = (input.query as string)?.toLowerCase().trim() || ''; const status = (input.status as string) || 'all'; @@ -2556,8 +2524,6 @@ export function createAdminToolHandlers( // Add industry feed handlers.set('add_industry_feed', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const name = (input.name as string)?.trim(); const feedUrl = (input.feed_url as string)?.trim(); @@ -2625,8 +2591,6 @@ export function createAdminToolHandlers( // Get feed stats handlers.set('get_feed_stats', async () => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; try { const stats = await getFeedStats(); @@ -2660,8 +2624,6 @@ export function createAdminToolHandlers( // List pending proposals handlers.set('list_feed_proposals', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const limit = Math.min(Math.max((input.limit as number) || 20, 1), 50); @@ -2699,8 +2661,6 @@ export function createAdminToolHandlers( // Approve a proposal handlers.set('approve_feed_proposal', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const proposalId = Number(input.proposal_id); const feedName = (input.feed_name as string)?.trim(); @@ -2769,8 +2729,6 @@ export function createAdminToolHandlers( // Reject a proposal handlers.set('reject_feed_proposal', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const proposalId = Number(input.proposal_id); const reason = input.reason as string | undefined; @@ -2802,8 +2760,6 @@ export function createAdminToolHandlers( // Add media contact handlers.set('add_media_contact', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const slackUserId = input.slack_user_id as string | undefined; const email = input.email as string | undefined; @@ -2852,8 +2808,6 @@ export function createAdminToolHandlers( // List flagged conversations handlers.set('list_flagged_conversations', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const unreviewedOnly = input.unreviewed_only !== false; // Default to true const severity = input.severity as 'high' | 'medium' | 'low' | undefined; @@ -2912,8 +2866,6 @@ export function createAdminToolHandlers( // Review flagged conversation handlers.set('review_flagged_conversation', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const flaggedId = input.flagged_id as number; const notes = input.notes as string | undefined; @@ -2948,8 +2900,6 @@ export function createAdminToolHandlers( // Grant discount to an organization handlers.set('grant_discount', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const orgId = input.org_id as string | undefined; const orgName = input.org_name as string | undefined; @@ -3062,8 +3012,6 @@ export function createAdminToolHandlers( // Remove discount from an organization handlers.set('remove_discount', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const orgId = input.org_id as string | undefined; const orgName = input.org_name as string | undefined; @@ -3119,8 +3067,6 @@ export function createAdminToolHandlers( // List organizations with active discounts handlers.set('list_discounts', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const limit = (input.limit as number) || 20; @@ -3170,8 +3116,6 @@ export function createAdminToolHandlers( // Create standalone promotion code handlers.set('create_promotion_code', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const code = input.code as string; const name = input.name as string | undefined; @@ -3268,8 +3212,6 @@ export function createAdminToolHandlers( // Create chapter handlers.set('create_chapter', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const name = (input.name as string)?.trim(); const region = (input.region as string)?.trim(); @@ -3347,8 +3289,6 @@ export function createAdminToolHandlers( // List chapters handlers.set('list_chapters', async () => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; try { const chapters = await wgDb.getChapters(); @@ -3392,8 +3332,6 @@ export function createAdminToolHandlers( // Create industry gathering handlers.set('create_industry_gathering', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const name = (input.name as string)?.trim(); const startDateStr = input.start_date as string; @@ -3506,8 +3444,6 @@ export function createAdminToolHandlers( // List industry gatherings handlers.set('list_industry_gatherings', async () => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; try { const gatherings = await wgDb.getIndustryGatherings(); @@ -3559,8 +3495,6 @@ export function createAdminToolHandlers( // Add committee leader handlers.set('add_committee_leader', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const committeeSlug = (input.committee_slug as string)?.trim(); let userId = (input.user_id as string)?.trim(); @@ -3633,8 +3567,6 @@ Committee management page: https://agenticadvertising.org/working-groups/${commi // Remove committee leader handlers.set('remove_committee_leader', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const committeeSlug = (input.committee_slug as string)?.trim(); let userId = (input.user_id as string)?.trim(); @@ -3685,8 +3617,6 @@ They are still a member but no longer have management access.`; // List committee leaders handlers.set('list_committee_leaders', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const committeeSlug = (input.committee_slug as string)?.trim(); @@ -3738,8 +3668,6 @@ Use add_committee_leader to assign a leader.`; // Merge organizations handlers.set('merge_organizations', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const primaryOrgId = input.primary_org_id as string; const secondaryOrgId = input.secondary_org_id as string; @@ -4000,8 +3928,6 @@ Use add_committee_leader to assign a leader.`; // Find duplicate organizations handlers.set('find_duplicate_orgs', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const searchType = (input.search_type as string) || 'all'; const pool = getPool(); @@ -4076,8 +4002,6 @@ Use add_committee_leader to assign a leader.`; // Manage organization domains handlers.set('manage_organization_domains', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const action = input.action as string; const organizationId = input.organization_id as string; @@ -4363,8 +4287,6 @@ Use add_committee_leader to assign a leader.`; // Update organization member role handlers.set('update_org_member_role', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const orgId = (input.org_id as string)?.trim(); const userId = (input.user_id as string)?.trim(); @@ -4453,8 +4375,6 @@ Use add_committee_leader to assign a leader.`; // Check domain health handlers.set('check_domain_health', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const checkType = (input.check_type as string) || 'all'; const limit = Math.min(Math.max((input.limit as number) || 20, 1), 100); @@ -4651,8 +4571,6 @@ Use add_committee_leader to assign a leader.`; // My engaged prospects - list owned prospects sorted by engagement handlers.set('my_engaged_prospects', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const limit = Math.min((input.limit as number) || 10, 50); @@ -4735,8 +4653,6 @@ Use add_committee_leader to assign a leader.`; // My followups needed - list owned prospects needing attention handlers.set('my_followups_needed', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const limit = Math.min((input.limit as number) || 10, 50); @@ -4826,8 +4742,6 @@ Use add_committee_leader to assign a leader.`; // Unassigned prospects - list high-engagement prospects without owners handlers.set('unassigned_prospects', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const limit = Math.min((input.limit as number) || 10, 50); @@ -4888,8 +4802,6 @@ Use add_committee_leader to assign a leader.`; // Claim prospect - assign self as owner handlers.set('claim_prospect', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); let orgId = input.org_id as string; @@ -4983,8 +4895,6 @@ Use add_committee_leader to assign a leader.`; // Suggest prospects - find unmapped domains and Lusha results handlers.set('suggest_prospects', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const limit = Math.min((input.limit as number) || 10, 20); @@ -5079,8 +4989,6 @@ Use add_committee_leader to assign a leader.`; // Set reminder - create a next step/reminder for a prospect handlers.set('set_reminder', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); let orgId = input.org_id as string; @@ -5216,8 +5124,6 @@ Use add_committee_leader to assign a leader.`; // My upcoming tasks - list future scheduled tasks handlers.set('my_upcoming_tasks', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); const limit = Math.min((input.limit as number) || 20, 50); @@ -5349,8 +5255,6 @@ Use add_committee_leader to assign a leader.`; // Log conversation - record an interaction and analyze for task management handlers.set('log_conversation', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const pool = getPool(); let orgId = input.org_id as string | undefined; @@ -5506,8 +5410,6 @@ Use add_committee_leader to assign a leader.`; // Get insight summary handlers.set('get_insight_summary', async (input) => { - const adminCheck = requireAdminFromContext(); - if (adminCheck) return adminCheck; const insightType = input.insight_type as string | undefined; const limit = (input.limit as number) || 5; @@ -5561,8 +5463,6 @@ Use add_committee_leader to assign a leader.`; // MEMBER SEARCH ANALYTICS HANDLERS // ============================================ handlers.set('get_member_search_analytics', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; try { const days = Math.min(Math.max((input.days as number) || 30, 1), 365); @@ -5673,8 +5573,6 @@ Use add_committee_leader to assign a leader.`; // ORGANIZATION ANALYTICS HANDLERS // ============================================ handlers.set('list_organizations_by_users', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; try { const pool = getPool(); @@ -5785,8 +5683,6 @@ Use add_committee_leader to assign a leader.`; // List Slack users for a specific organization handlers.set('list_slack_users_by_org', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; try { const pool = getPool(); @@ -5937,8 +5833,6 @@ Use add_committee_leader to assign a leader.`; // ============================================ handlers.set('tag_insight', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; try { const content = input.content as string; @@ -5993,8 +5887,6 @@ Use add_committee_leader to assign a leader.`; }); handlers.set('list_pending_insights', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; try { const topic = (input.topic as string) || undefined; @@ -6043,8 +5935,6 @@ Use add_committee_leader to assign a leader.`; }); handlers.set('run_synthesis', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; try { const topic = (input.topic as string) || undefined; @@ -6126,8 +6016,6 @@ Use add_committee_leader to assign a leader.`; // LIST ESCALATIONS // ============================================ handlers.set('list_escalations', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; try { const status = (input.status as EscalationStatus) || 'open'; @@ -6187,8 +6075,6 @@ Use add_committee_leader to assign a leader.`; // RESOLVE ESCALATION // ============================================ handlers.set('resolve_escalation', async (input) => { - const adminError = requireAdminFromContext(); - if (adminError) return adminError; const escalationId = input.escalation_id; if (typeof escalationId !== 'number' || !Number.isInteger(escalationId) || escalationId < 1) { diff --git a/server/src/addie/mcp/event-tools.ts b/server/src/addie/mcp/event-tools.ts index 4b2ed3908d..6f3a032ff3 100644 --- a/server/src/addie/mcp/event-tools.ts +++ b/server/src/addie/mcp/event-tools.ts @@ -13,7 +13,7 @@ import { createLogger } from '../../logger.js'; import type { AddieTool } from '../types.js'; import type { MemberContext } from '../member-context.js'; -import { isSlackUserAdmin } from './admin-tools.js'; +import { isSlackUserAAOAdmin } from './admin-tools.js'; import { eventsDb } from '../../db/events-db.js'; import { query } from '../../db/client.js'; import { @@ -287,7 +287,7 @@ async function getPersonalizedEvents( */ export async function canCreateEvents(slackUserId: string): Promise { // Admins can always create events - const isAdmin = await isSlackUserAdmin(slackUserId); + const isAdmin = await isSlackUserAAOAdmin(slackUserId); if (isAdmin) return true; // TODO: Check committee membership when that's implemented diff --git a/server/src/addie/mcp/meeting-tools.ts b/server/src/addie/mcp/meeting-tools.ts index 6c3be4ab62..995676bf09 100644 --- a/server/src/addie/mcp/meeting-tools.ts +++ b/server/src/addie/mcp/meeting-tools.ts @@ -15,7 +15,7 @@ import type { AddieTool } from '../types.js'; import type { MemberContext } from '../member-context.js'; import type { ThreadContext } from '../thread-service.js'; import type { RecurrenceRule, CreateMeetingSeriesInput } from '../../types.js'; -import { isSlackUserAdmin } from './admin-tools.js'; +import { isSlackUserAAOAdmin, isWebUserAAOAdmin } from './admin-tools.js'; import { MeetingsDatabase } from '../../db/meetings-db.js'; import { WorkingGroupDatabase } from '../../db/working-group-db.js'; import * as meetingService from '../../services/meeting-service.js'; @@ -33,7 +33,7 @@ const workingGroupDb = new WorkingGroupDatabase(); */ export async function canScheduleMeetings(slackUserId: string): Promise { // Admins can always schedule - const isAdmin = await isSlackUserAdmin(slackUserId); + const isAdmin = await isSlackUserAAOAdmin(slackUserId); if (isAdmin) return true; // Check if user is a leader of any working group @@ -574,15 +574,15 @@ export function createMeetingToolHandlers( // This check runs for both Slack and web channels const userId = getUserId(); if (userId) { - // Determine admin status from either Slack or web context - let isAdmin = false; + // Determine AAO admin status from either Slack or web context + let isAAOAdmin = false; if (slackUserId) { - isAdmin = await isSlackUserAdmin(slackUserId); - } else if (memberContext?.org_membership?.role === 'admin') { - isAdmin = true; + isAAOAdmin = await isSlackUserAAOAdmin(slackUserId); + } else if (memberContext?.workos_user?.workos_user_id) { + isAAOAdmin = await isWebUserAAOAdmin(memberContext.workos_user.workos_user_id); } - if (!isAdmin) { + if (!isAAOAdmin) { const isGroupLeader = await workingGroupDb.isLeader(workingGroup.id, userId); if (!isGroupLeader) { return `⚠️ You can only schedule meetings for committees you lead. You're not a leader of "${workingGroup.name}".`; diff --git a/server/src/addie/member-context.ts b/server/src/addie/member-context.ts index ad87aba192..841e3de394 100644 --- a/server/src/addie/member-context.ts +++ b/server/src/addie/member-context.ts @@ -32,7 +32,7 @@ const joinRequestDb = new JoinRequestDatabase(); */ async function getPendingContentForUser( workosUserId: string, - isAdmin: boolean + isAAOAdmin: boolean ): Promise<{ total: number; by_committee: Record }> { const pool = getPool(); @@ -48,7 +48,7 @@ async function getPendingContentForUser( ); const ledCommitteeIds = leaderResult.rows.map(c => c.id); - if (!isAdmin && ledCommitteeIds.length === 0) { + if (!isAAOAdmin && ledCommitteeIds.length === 0) { return { total: 0, by_committee: {} }; } @@ -61,7 +61,7 @@ async function getPendingContentForUser( `; const params: (string | string[])[] = []; - if (!isAdmin) { + if (!isAAOAdmin) { // Non-admins only see pending for committees they lead params.push(ledCommitteeIds); query += ` AND p.working_group_id = ANY($${params.length})`; @@ -474,15 +474,18 @@ export async function getMemberContext(slackUserId: string): Promise wg.is_leader) .map(wg => wg.name) || []; - const userIsAdmin = context.org_membership?.role === 'admin'; - if (ledCommitteeIds.length > 0 || userIsAdmin) { + // Check AAO admin status (aao-admin working group membership) + const adminGroup = await workingGroupDb.getWorkingGroupBySlug('aao-admin'); + const isAAOAdmin = adminGroup ? await workingGroupDb.isMember(adminGroup.id, workosUserId) : false; + + if (ledCommitteeIds.length > 0 || isAAOAdmin) { try { - const pendingContent = await getPendingContentForUser(workosUserId, userIsAdmin); + const pendingContent = await getPendingContentForUser(workosUserId, isAAOAdmin); if (pendingContent.total > 0) { context.pending_content = pendingContent; } @@ -491,8 +494,9 @@ export async function getMemberContext(slackUserId: string): Promise 0) { @@ -757,13 +761,16 @@ export async function getWebMemberContext(workosUserId: string): Promise wg.is_leader) || []; - const webUserIsAdmin = context.org_membership?.role === 'admin'; - if (leadsCommittees.length > 0 || webUserIsAdmin) { + // Check AAO admin status (aao-admin working group membership) + const webAdminGroup = await workingGroupDb.getWorkingGroupBySlug('aao-admin'); + const webIsAAOAdmin = webAdminGroup ? await workingGroupDb.isMember(webAdminGroup.id, workosUserId) : false; + + if (leadsCommittees.length > 0 || webIsAAOAdmin) { try { - const pendingContent = await getPendingContentForUser(workosUserId, webUserIsAdmin); + const pendingContent = await getPendingContentForUser(workosUserId, webIsAAOAdmin); if (pendingContent.total > 0) { context.pending_content = pendingContent; } @@ -772,8 +779,9 @@ export async function getWebMemberContext(workosUserId: string): Promise 0) { diff --git a/server/src/addie/prompts.ts b/server/src/addie/prompts.ts index 050f8c7622..59fef29236 100644 --- a/server/src/addie/prompts.ts +++ b/server/src/addie/prompts.ts @@ -192,12 +192,12 @@ export const STATUS_MESSAGES = { * Build dynamic suggested prompts based on user context, role, and active goals * * @param memberContext - User's member context (or null if lookup failed) - * @param isAdmin - Whether the user has admin privileges + * @param isAAOAdmin - Whether the user is an AAO platform admin * @returns Array of suggested prompts tailored to the user */ export async function buildDynamicSuggestedPrompts( memberContext: MemberContext | null, - isAdmin: boolean + isAAOAdmin: boolean ): Promise { const isMapped = !!memberContext?.workos_user?.workos_user_id; @@ -223,7 +223,7 @@ export async function buildDynamicSuggestedPrompts( } // Admin users get admin-specific suggestions - if (isAdmin) { + if (isAAOAdmin) { return [ { title: 'Pending invoices', diff --git a/server/src/addie/router.ts b/server/src/addie/router.ts index 9881efdd79..36926e66f5 100644 --- a/server/src/addie/router.ts +++ b/server/src/addie/router.ts @@ -71,6 +71,8 @@ export interface RoutingContext { channelName?: string; /** Member insights (what we know about this user from past conversations) */ memberInsights?: MemberInsight[]; + /** Whether the user is an AAO platform admin (checked via aao-admin working group) */ + isAAOAdmin?: boolean; } /** @@ -294,12 +296,12 @@ Use these insights to: * Build the routing prompt based on context */ function buildRoutingPrompt(ctx: RoutingContext): string { - const isAdmin = ctx.memberContext?.org_membership?.role === 'admin'; + const isAAOAdmin = ctx.isAAOAdmin ?? false; const isMember = !!ctx.memberContext?.workos_user?.workos_user_id; const isLinked = isMember; // Build tool SET descriptions - router selects categories, not individual tools - const toolSetsSection = getToolSetDescriptionsForRouter(isAdmin); + const toolSetsSection = getToolSetDescriptionsForRouter(isAAOAdmin); // Build react patterns const reactList = Object.entries(ROUTING_RULES.reactWith) @@ -316,7 +318,7 @@ function buildRoutingPrompt(ctx: RoutingContext): string { The user has NOT linked their Slack account to AgenticAdvertising.org. - If they ask about membership features, include the "member" tool set`; } - if (isAdmin) { + if (isAAOAdmin) { conditionalRules += ` The user is an ADMIN. - They have access to the "admin" tool set for system operations @@ -328,7 +330,7 @@ The user is an ADMIN. ## User Context - Source: ${ctx.source} - Is member: ${isMember} -- Is admin: ${isAdmin} +- Is admin: ${isAAOAdmin} - In thread: ${ctx.isThread ?? false} ${conditionalRules} ${insightsSection} @@ -378,7 +380,7 @@ Respond with a JSON object for the execution plan. Choose ONE action: 4. {"action": "respond", "tool_sets": ["set1", "set2"], "reason": "brief reason"} - When you can help - select the tool SET(S) that will be needed - - Valid sets: knowledge, member, directory, agent_testing, adcp_operations, content, billing, meetings${isAdmin ? ', admin' : ''} + - Valid sets: knowledge, member, directory, agent_testing, adcp_operations, content, billing, meetings${isAAOAdmin ? ', admin' : ''} - Empty array [] means respond without tools (general knowledge) Respond with ONLY the JSON object, no other text.`; diff --git a/server/src/addie/tool-sets.ts b/server/src/addie/tool-sets.ts index 592dc9c3fc..a738165d60 100644 --- a/server/src/addie/tool-sets.ts +++ b/server/src/addie/tool-sets.ts @@ -298,14 +298,14 @@ export function getToolsInSet(setName: string): string[] { /** * Get all tool names for multiple sets, including always-available tools */ -export function getToolsForSets(setNames: string[], isAdmin: boolean = false): string[] { +export function getToolsForSets(setNames: string[], isAAOAdmin: boolean = false): string[] { const tools = new Set(ALWAYS_AVAILABLE_TOOLS); for (const setName of setNames) { const toolSet = TOOL_SETS[setName]; if (toolSet) { // Skip admin-only sets if user is not admin - if (toolSet.adminOnly && !isAdmin) { + if (toolSet.adminOnly && !isAAOAdmin) { continue; } for (const tool of toolSet.tools) { @@ -320,10 +320,10 @@ export function getToolsForSets(setNames: string[], isAdmin: boolean = false): s /** * Get tool set names that were NOT selected (for hinting to Sonnet) */ -export function getUnavailableSets(selectedSets: string[], isAdmin: boolean = false): string[] { +export function getUnavailableSets(selectedSets: string[], isAAOAdmin: boolean = false): string[] { return Object.keys(TOOL_SETS).filter(setName => { // Don't mention admin set to non-admins - if (TOOL_SETS[setName].adminOnly && !isAdmin) { + if (TOOL_SETS[setName].adminOnly && !isAAOAdmin) { return false; } return !selectedSets.includes(setName); @@ -334,8 +334,8 @@ export function getUnavailableSets(selectedSets: string[], isAdmin: boolean = fa * Build a hint message about unavailable tool sets * This helps Sonnet know it can redirect the user if needed */ -export function buildUnavailableSetsHint(selectedSets: string[], isAdmin: boolean = false): string { - const unavailable = getUnavailableSets(selectedSets, isAdmin); +export function buildUnavailableSetsHint(selectedSets: string[], isAAOAdmin: boolean = false): string { + const unavailable = getUnavailableSets(selectedSets, isAAOAdmin); if (unavailable.length === 0) { return ''; @@ -368,9 +368,9 @@ export function requiresPrecision(selectedSets: string[]): boolean { /** * Get tool set descriptions for the router prompt */ -export function getToolSetDescriptionsForRouter(isAdmin: boolean = false): string { +export function getToolSetDescriptionsForRouter(isAAOAdmin: boolean = false): string { return Object.entries(TOOL_SETS) - .filter(([_, set]) => !set.adminOnly || isAdmin) + .filter(([_, set]) => !set.adminOnly || isAAOAdmin) .map(([name, set]) => `- **${name}**: ${set.description}`) .join('\n'); } diff --git a/server/src/http.ts b/server/src/http.ts index be5e508373..24b9d9fe58 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -52,7 +52,7 @@ import { createMoltbookAdminRouter } from "./routes/moltbook-admin.js"; import { createAddieChatRouter } from "./routes/addie-chat.js"; import { createSiChatRoutes } from "./routes/si-chat.js"; import { sendAccountLinkedMessage, invalidateMemberContextCache, getAddieBoltRouter, isAddieBoltReady } from "./addie/index.js"; -import { isWebUserAdmin } from "./addie/mcp/admin-tools.js"; +import { isWebUserAAOAdmin } from "./addie/mcp/admin-tools.js"; import { createSlackRouter } from "./routes/slack.js"; import { createWebhooksRouter } from "./routes/webhooks.js"; import { createWorkOSWebhooksRouter } from "./routes/workos-webhooks.js"; @@ -2120,7 +2120,7 @@ export class HTTPServer { // Check ownership - user must be creator or admin const isCreator = brand.created_by_user_id && brand.created_by_user_id === req.user?.id; - const isAdmin = req.user && await isWebUserAdmin(req.user.id); + const isAdmin = req.user && await isWebUserAAOAdmin(req.user.id); if (!isCreator && !isAdmin) { return res.status(403).json({ error: 'Not authorized to update this brand' }); } @@ -2170,7 +2170,7 @@ export class HTTPServer { // Check ownership - user must be creator or admin const isCreator = brand.created_by_user_id && brand.created_by_user_id === req.user?.id; - const isAdmin = req.user && await isWebUserAdmin(req.user.id); + const isAdmin = req.user && await isWebUserAAOAdmin(req.user.id); if (!isCreator && !isAdmin) { return res.status(403).json({ error: 'Not authorized to delete this brand' }); } @@ -2342,7 +2342,7 @@ export class HTTPServer { // Check ownership const isCreator = property.created_by_email && property.created_by_email === req.user?.email; - const isAdmin = req.user && await isWebUserAdmin(req.user.id); + const isAdmin = req.user && await isWebUserAAOAdmin(req.user.id); if (!isCreator && !isAdmin) { return res.status(403).json({ error: 'Not authorized to delete this property' }); } @@ -2526,7 +2526,7 @@ export class HTTPServer { // Check if user can delete (admin or creator) const devUser = getDevUser(req); const isDevAdmin = devUser?.isAdmin === true; - const isDbAdmin = req.user && await isWebUserAdmin(req.user.id); + const isDbAdmin = req.user && await isWebUserAAOAdmin(req.user.id); const isAdmin = isDevAdmin || isDbAdmin; const isCreator = ref.contributed_by_email === req.user?.email; diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index b171c3f29c..955e79515b 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -3,7 +3,7 @@ import { WorkOS } from '@workos-inc/node'; import { CompanyDatabase } from '../db/company-db.js'; import type { WorkOSUser, Company, CompanyUser } from '../types.js'; import { createLogger } from '../logger.js'; -import { isWebUserAdmin } from '../addie/mcp/admin-tools.js'; +import { isWebUserAAOAdmin } from '../addie/mcp/admin-tools.js'; const logger = createLogger('auth-middleware'); @@ -759,7 +759,7 @@ export async function requireAdmin(req: Request, res: Response, next: NextFuncti // or ADMIN_EMAILS env var (fallback for emergency access) const adminEmails = process.env.ADMIN_EMAILS?.split(',').map(e => e.trim().toLowerCase()) || []; const isAdminByEmail = adminEmails.includes(req.user.email.toLowerCase()); - const isAdminByWorkingGroup = await isWebUserAdmin(req.user.id); + const isAdminByWorkingGroup = await isWebUserAAOAdmin(req.user.id); const isAdmin = isAdminByWorkingGroup || isAdminByEmail; if (!isAdmin) { diff --git a/server/src/routes/content.ts b/server/src/routes/content.ts index 8d9319d3a9..1b496f881a 100644 --- a/server/src/routes/content.ts +++ b/server/src/routes/content.ts @@ -12,7 +12,7 @@ import { Router } from 'express'; import { createLogger } from '../logger.js'; import { requireAuth } from '../middleware/auth.js'; import { getPool } from '../db/client.js'; -import { isWebUserAdmin } from '../addie/mcp/admin-tools.js'; +import { isWebUserAAOAdmin } from '../addie/mcp/admin-tools.js'; import { sendChannelMessage } from '../slack/client.js'; import { notifyPublishedPost } from '../notifications/slack.js'; @@ -213,7 +213,7 @@ export async function proposeContentForUser( // Check if user can submit to this collection const userIsLead = await isCommitteeLead(committeeId, user.id); - const userIsAdmin = await isWebUserAdmin(user.id); + const userIsAdmin = await isWebUserAAOAdmin(user.id); // For non-public collections, user must be a member if (!acceptsPublicSubmissions && !userIsLead && !userIsAdmin) { @@ -451,7 +451,7 @@ export function createContentRouter(): Router { const ledCommitteeIds = ledCommittees.map(c => c.id); // Check if admin - const userIsAdmin = await isWebUserAdmin(user.id); + const userIsAdmin = await isWebUserAAOAdmin(user.id); if (!userIsAdmin && ledCommitteeIds.length === 0) { return res.json({ @@ -573,7 +573,7 @@ export function createContentRouter(): Router { } // Check permission - const userIsAdmin = await isWebUserAdmin(user.id); + const userIsAdmin = await isWebUserAAOAdmin(user.id); const userIsLead = content.working_group_id ? await isCommitteeLead(content.working_group_id, user.id) : false; @@ -767,7 +767,7 @@ export function createContentRouter(): Router { } // Check permission - const userIsAdmin = await isWebUserAdmin(user.id); + const userIsAdmin = await isWebUserAAOAdmin(user.id); const userIsLead = content.working_group_id ? await isCommitteeLead(content.working_group_id, user.id) : false; @@ -980,7 +980,7 @@ export function createMyContentRouter(): Router { const userIsLead = contentItem.working_group_id ? await isCommitteeLead(contentItem.working_group_id, user.id) : false; - const userIsAdmin = await isWebUserAdmin(user.id); + const userIsAdmin = await isWebUserAAOAdmin(user.id); if (!isProposer && !isAuthor && !userIsLead && !userIsAdmin) { return res.status(403).json({ @@ -1092,7 +1092,7 @@ export function createMyContentRouter(): Router { const userIsLead = contentItem.working_group_id ? await isCommitteeLead(contentItem.working_group_id, user.id) : false; - const userIsAdmin = await isWebUserAdmin(user.id); + const userIsAdmin = await isWebUserAAOAdmin(user.id); if (!isProposer && !userIsLead && !userIsAdmin) { return res.status(403).json({ @@ -1162,7 +1162,7 @@ export function createMyContentRouter(): Router { const userIsLead = contentItem.working_group_id ? await isCommitteeLead(contentItem.working_group_id, user.id) : false; - const userIsAdmin = await isWebUserAdmin(user.id); + const userIsAdmin = await isWebUserAAOAdmin(user.id); if (!isProposer && !userIsLead && !userIsAdmin) { return res.status(403).json({