--- name: ai-agent description: Quick reference for the Breeze RMM AI Agent system architecture, MCP tools, streaming chat, cost tracking, guardrails, and MCP server. Use when working on AI features, debugging chat issues, adding new tools, or understanding the AI data flow. --- # Breeze RMM AI Agent System Reference Integrated AI agent allowing IT technicians to manage devices, troubleshoot issues, analyze security, and build automations through natural language chat. ## Architecture Overview ``` External Clients (Claude Desktop, Cursor) | MCP over Streamable HTTP (legacy SSE kept) v +---------------------------+ | Breeze MCP Server | /api/v1/mcp/* (API Key auth) +---------------------------+ | +------------------------------------------------------------------+ | Hono API Server | | | | AI Chat Routes MCP Server Routes | | /api/v1/ai/* /api/v1/mcp/* | | | | | | +--------------------------------------------------+ | | | AI Agent Service (aiAgent.ts) | | | | Anthropic SDK + In-Process Tool Execution | | | | | | | | AI tool registry (aiTools*.ts) | | | | getToolDefinitions() supplies the current tools | | | +--------------------------------------------------+ | | | | | | | Existing Services commandQueue.ts Event Bus (Redis) | +------------------------------------------------------------------+ | | PostgreSQL Go Agent (on devices) ``` ## File Map ### Backend (API) | File | Purpose | |------|---------| | `apps/api/src/db/schema/ai.ts` | 5 tables: `aiSessions`, `aiMessages`, `aiToolExecutions`, `aiCostUsage`, `aiBudgets` | | `apps/api/src/services/aiAgent.ts` | Core agent service — session lifecycle, Anthropic API calls, SSE streaming, tool dispatch, approval polling | | `apps/api/src/services/aiTools.ts` | The AI tool registry (~200 tools across `aiTools*.ts`; `getToolDefinitions()` is the count), with Zod schemas and org-scoped access via `AuthContext.orgCondition()` | | `apps/api/src/services/aiGuardrails.ts` | 4-tier permission system: auto-execute, audit, approval-required, blocked | | `apps/api/src/services/aiCostTracker.ts` | Token/cost tracking, budget enforcement, rate limiting, usage summaries | | `apps/api/src/routes/ai.ts` | REST + SSE chat endpoints (`/api/v1/ai/*`) | | `apps/api/src/routes/mcpServer.ts` | External MCP server for Claude Desktop/Cursor (`/api/v1/mcp/*`) | ### Frontend (Web) | File | Purpose | |------|---------| | `apps/web/src/stores/aiStore.ts` | Zustand store — sessions, messages, streaming, context, approval state | | `apps/web/src/components/ai/AiChatSidebar.tsx` | Slide-out panel (right side), session management, conversation history | | `apps/web/src/components/ai/AiChatMessages.tsx` | Scrollable message list with markdown rendering | | `apps/web/src/components/ai/AiChatInput.tsx` | Auto-resize textarea, Cmd+Enter send | | `apps/web/src/components/ai/AiToolCallCard.tsx` | Collapsible tool invocation display with input/output | | `apps/web/src/components/ai/AiApprovalDialog.tsx` | Approve/Reject card for Tier 3 tool executions | | `apps/web/src/components/ai/AiContextBadge.tsx` | Shows current page context injected into chat | | `apps/web/src/components/ai/AiCostIndicator.tsx` | Token usage + budget remaining (with polling circuit breaker) | | `apps/web/src/components/settings/AiUsagePage.tsx` | Admin dashboard for AI usage and budget configuration | | `apps/web/src/pages/settings/ai-usage.astro` | Astro page wrapper for AI usage admin | ### Shared | File | Purpose | |------|---------| | `packages/shared/src/types/ai.ts` | TypeScript interfaces: `AiSession`, `AiMessage`, `AiToolExecution`, `AiPageContext`, `AiStreamEvent` | | `packages/shared/src/validators/ai.ts` | Zod schemas for AI page context and message validation | ## Database Schema 5 tables in `apps/api/src/db/schema/ai.ts`: | Table | Key Columns | Purpose | |-------|-------------|---------| | `ai_sessions` | orgId, userId, status, model, turnCount, totalCostCents | Multi-turn conversations | | `ai_messages` | sessionId, role, content, toolName, toolInput, toolOutput | Message history | | `ai_tool_executions` | sessionId, toolName, status, approvedBy, commandId | Tool audit trail | | `ai_cost_usage` | orgId, period, periodKey, inputTokens, outputTokens, totalCostCents | Daily/monthly cost aggregates | | `ai_budgets` | orgId, enabled, monthlyBudgetCents, dailyBudgetCents, maxTurnsPerSession | Per-org budget config | Enums: `ai_session_status` (active/closed/expired), `ai_message_role` (user/assistant/system/tool_use/tool_result), `ai_tool_status` (pending/approved/executing/completed/failed/rejected) ## API Routes ### Chat Routes (`/api/v1/ai/*`) | Method | Path | Purpose | |--------|------|---------| | POST | `/sessions` | Create session (with optional page context) | | GET | `/sessions` | List user's sessions (filterable by status) | | GET | `/sessions/search` | Search past conversations (must be before `:id` route!) | | GET | `/sessions/:id` | Get session with message history | | DELETE | `/sessions/:id` | Close session | | POST | `/sessions/:id/messages` | Send message, returns SSE stream | | POST | `/sessions/:id/approve/:executionId` | Approve/reject tool execution | | GET | `/usage` | Usage + budget summary for org | | PUT | `/budget` | Update budget settings | | GET | `/admin/sessions` | Session history for admin dashboard | ### MCP Server Routes (`/api/v1/mcp/*`) | Method | Path | Auth | Purpose | |--------|------|------|---------| | GET | `/sse` | API Key (`ai:*`) | SSE transport (server→client) | | POST | `/message` | API Key (`ai:*`) | JSON-RPC messages (client→server) | Session ownership validated — each SSE session tracks `apiKeyId`. Max 100 sessions, 30-min TTL. ## SSE Stream Events Events emitted by `POST /sessions/:id/messages`: ```typescript type AiStreamEvent = | { type: 'message_start'; messageId: string } | { type: 'content_delta'; delta: string } | { type: 'tool_use_start'; toolName: string; toolUseId: string; input: Record } | { type: 'tool_result'; toolUseId: string; output: unknown; isError: boolean } | { type: 'approval_required'; executionId: string; toolName: string; input: Record; description: string } | { type: 'message_end'; inputTokens: number; outputTokens: number } | { type: 'error'; message: string } | { type: 'done' }; ``` ## MCP tools Use the registry in `apps/api/src/services/aiTools.ts` (`getToolDefinitions()`) for the current tools and count, and the generated prompt index `renderToolIndexByDomain(listChatSurfaceToolNames())` composed in `apps/api/src/services/aiAgent.ts` for the tools registered on the chat surface. ### Helper Functions in aiTools.ts | Helper | Purpose | |--------|---------| | `verifyDeviceAccess(deviceId, auth, requireOnline?)` | Device lookup + org condition check | | `findAlertWithAccess(alertId, auth)` | Alert lookup + org condition check | | `getCommandQueue()` | Cached dynamic import of commandQueue module | | `getToolTier(toolName)` | Returns numeric tier for guardrails | | `executeTool(toolName, input, auth)` | Main dispatch — throws for unknown tools | ### Result envelopes (A-W05) Every shaped list/search tool adds one additive envelope on top of its existing keys, built by the shared helper in `apps/api/src/services/aiToolPagination.ts` — never a bespoke per-tool shape. - **Offset mode** (`readPageArgs`/`pageEnvelope`) — stable lists (devices, tickets, patches, scripts, policies, …): `limit`, `offset`, optional `cursor`; result adds `showing`, `hasMore`, `nextCursor` (and `total`/`totalMode` where the query already counts). - **Keyset mode** (`readKeysetArgs`/`keysetEnvelope`) — churn tables where an insert between pages would shift offsets (alerts, agent logs, audit log, change log): `limit`, `cursor` (no `offset`); sorted `(col DESC, id DESC)` on a microsecond-precision `timestamp` column carried as text in the cursor, never round-tripped through JS `Date`. A cursor minted for different filters is refused with `CURSOR_MISMATCH`, not silently reset to page 1. - Both are additive: no top-level key is ever removed, and a tool is never renamed. Heavy per-row fields (jsonb `settings`/`details`, script bodies, nested sub-arrays) are dropped from the default page behind an opt-in flag (e.g. `includeDetails`) or left to the matching `get_*` detail tool, with a `Count` in their place. - `compactToolResultForChat` (`aiToolOutput.ts`) learns whether a tool can page through an **injected resolver** (`setToolPaginationHintResolver`, installed by `aiTools.ts` after registration) — never by importing the tool registry directly. Its `_chat.nextStep` hint and the array-truncation sentinel only ever suggest pagination to a tool that actually supports it. - A new list tool: add paging with `pageParamSchema()`/`pageZodShape()` (or the keyset equivalents) on **all three** input-schema surfaces — the registry `definition.input_schema`, `aiToolSchemas.ts` (or `aiToolSchemasFleet.ts`), and the `tool()` Zod shape in `aiAgentSdkTools.ts` (plus `scriptBuilderTools.ts` if the tool has its own SDK shape there) — or the model gets a parameter the schema strips and pages nowhere. - `read_artifact` (`aiToolsArtifacts.ts`) pages a stored oversized result back by byte offset via `artifact.handle` in an earlier tool result. It only reads an artifact captured by the caller's own chat session or agent run (never any session of the same user), and only works on hosted deployments with the AI workspace enabled — capture stores the **redacted** payload, never raw. MCP `tools/call` has no session anchor today, so `read_artifact` called over MCP typically refuses with `no_capture_anchor`. ## Guardrails (4-Tier System) Defined in `aiGuardrails.ts`: | Tier | Behavior | Examples | |------|----------|---------| | **1** | Auto-execute | query_devices, get_device_details, analyze_metrics, query_audit_log | | **2** | Auto-execute + audit log | manage_alerts(acknowledge/resolve), manage_services(list) | | **3** | Requires user approval (5-min timeout) | execute_command, run_script, file writes, service start/stop | | **4** | Blocked | Unknown tools, auth modifications | Action-based escalation: `TIER3_ACTIONS` maps tool+action combos that escalate (e.g., `file_operations.write` escalates from T1 to T3). ## Approval Flow 1. AI invokes a Tier 3 tool 2. `aiAgent.ts` inserts `aiToolExecutions` record with status `'pending'` 3. SSE emits `approval_required` event with tool name, input, description 4. Frontend shows `AiApprovalDialog` inline in chat 5. User clicks Approve/Reject -> `POST /sessions/:id/approve/:executionId` 6. `waitForApproval()` polls DB every 2s with circuit breaker (max 5 consecutive DB errors) 7. 5-minute auto-reject timeout 8. On approval: existing record updated to `'executing'`, tool runs, result returned ## Cost Tracking In `aiCostTracker.ts`: | Function | Purpose | |----------|---------| | `calculateCostCents(model, inputTokens, outputTokens)` | Claude pricing math | | `checkBudget(orgId)` | Pre-message budget check (fails closed on DB errors) | | `checkAiRateLimit(userId, orgId)` | 20 msg/min per user, 200 msg/hr per org | | `recordUsage(sessionId, orgId, model, inputTokens, outputTokens, isToolExecution)` | Atomic transaction: session update + daily/monthly aggregate upserts | | `updateBudget(orgId, settings)` | Upsert budget configuration | | `getUsageSummary(orgId)` | Daily + monthly usage + budget for admin dashboard | | `getSessionHistory(orgId, { limit, offset })` | Session list for admin | Model pricing (cents per million tokens): - `claude-sonnet-4-5-20250929`: 300 input / 1500 output - `claude-haiku-4-5-20251001`: 100 input / 500 output ## Page Context Injection Frontend pushes context to `aiStore.setPageContext()`: ```typescript type AiPageContext = | { type: 'device'; id: string; hostname: string; os?: string; status?: string; ip?: string } | { type: 'alert'; id: string; title: string; severity?: string; deviceHostname?: string } | { type: 'dashboard'; orgName?: string; deviceCount?: number; alertCount?: number } | { type: 'custom'; label: string; data: Record }; ``` Context is injected into the system prompt and sent with each message. The `AiContextBadge` component shows it visually in the chat header. ## Data Flow: Chat Message ``` 1. User types message in AiChatInput 2. aiStore.sendMessage(content) - Creates session if needed (POST /sessions) - Adds user message optimistically - POST /sessions/:id/messages with SSE stream 3. API receives message - checkAiRateLimit() — fail closed on Redis error - checkBudget() — fail closed on DB error - Insert user message to ai_messages - Build Anthropic messages array from conversation history - Call Anthropic API with tools + system prompt 4. Stream processing (generator function) - Yield message_start, content_delta events - On tool_use: check guardrails - Tier 1-2: execute immediately - Tier 3: insert execution record, yield approval_required, wait - Tier 4: return error - Yield tool_result events - On message_end: recordUsage() in transaction - Yield done 5. Frontend processes SSE events via processStreamEvent() - Updates Zustand store incrementally - Shows tool cards, approval dialogs, streaming text ``` ## Key Patterns & Safety - **Fail-closed**: Rate limit and budget checks deny requests when Redis/DB is down - **Circuit breaker**: `waitForApproval` breaks after 5 consecutive DB poll failures - **ILIKE escaping**: `escapeLike()` helper prevents SQL wildcard injection in search - **Transaction wrapping**: `recordUsage` uses `db.transaction()` for atomicity - **Session ownership**: MCP SSE sessions track `apiKeyId` to prevent cross-session injection - **Polling circuit breaker**: `AiCostIndicator` stops after 5 failures or auth errors - **Route ordering**: `/sessions/search` registered before `/sessions/:id` in Hono - **No dual records**: Approval flow updates existing execution record instead of creating a second one ## Connecting External Clients ```bash # Claude Code MCP connection claude mcp add --transport http breeze-rmm https://your-breeze-instance.example.com/api/v1/mcp/sse \ --header "X-API-Key: brz_your_api_key_here" ``` API key needs `ai:read`, `ai:write`, or `ai:execute` scopes.