MM-65671: Agents CRUD, legacy bot migration, and bot account sync#589
Conversation
Detailed implementation plan for Agents_UserAgents table, Morph migration, CRUD store methods, and integration tests. Includes exact file paths, line numbers, code snippets, and verification commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 of self-service agent creation: Agents_UserAgents table with Morph migration 000004, full CRUD store methods with JSON slice field marshaling, AgentStore interface in api package, and comprehensive integration tests (12 test functions covering round-trips, soft delete, edge cases, and concurrency). Also fixes setupTestStore to set search_path via connection string so concurrent test goroutines use the correct schema, and updates migration count assertion from 3 to 4. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements Phase 2 of self-service agent creation: - Agent CRUD handlers (create, list, get, update, delete) with license gating - Bot account lifecycle (create on agent create, deactivate on delete) - Avatar upload endpoint for agent profile images - Services listing endpoint (secrets stripped) - Permission-based access control (create_agent permission, creator/admin checks) - Partial update support via pointer fields in UpdateAgentRequest - Comprehensive test coverage (9 test functions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements Phase 3 of self-service agent creation: - AgentStore interface in bots package for loading user agents from DB - userAgentToBotConfig converter maps UserAgent to llm.BotConfig - EnsureBots merges DB-backed agents into the live bot registry - forceRefresh flag bypasses optimistic config-equality cache for agent CRUD - clusterEventAgentUpdate event propagates agent changes to HA nodes - ClusterAgentNotifier interface + refreshBotsAndNotify helper in API - Agent create/update/delete handlers trigger registry refresh + cluster notify - Runtime tests: converter, registry lookup, force-refresh flag - Nil config guard in EnsureBots for test safety Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add TypeScript types for UserAgent, CreateAgentRequest, UpdateAgentRequest, ServiceInfo - Add agent CRUD API client functions (getAgents, createAgent, updateAgent, deleteAgent, uploadAgentAvatar, getServices) - Add Redux agents reducer for state caching - Create AgentsPage root component with URL-based visibility at /plug/mattermost-ai/agents - Create AgentRow component with avatar, name, tool badge, and actions menu - Create AgentsList component with All/Your agents tabs, loading/error/empty states, and delete flow - Create DeleteAgentDialog confirmation modal - Create AgentsLicenseGate for E20+ license enforcement - Register root component and main menu navigation entry point Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. getServices() was calling /agents/services but the backend route is /services on the base router — the gin router would match /agents/services against /:agentid with agentid="services" → 404. 2. EnabledTool TS type had mismatched fields (type/id/name/serverName) vs Go struct (server_origin/tool_name). Aligned to match backend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 5 implementation: three-tab modal for creating/editing agents. - Config tab: display name, username, avatar, service selection, custom instructions - Access tab: channel access, user access, agent admin controls - MCPs tab: per-server and per-tool toggle switches with search - Wire modal into agents_list.tsx for create and edit flows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Go UserAgent struct uses snake_case JSON tags (bot_user_id, creator_id, display_name, etc.) but the TS type used camelCase, causing JSON deserialization to silently drop all fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire UserAgent.EnabledTools into the tool discovery pipeline so DB-backed agents only expose their selected MCP tools. Adds EnabledMCPTool type to BotConfig, convertEnabledTools mapping, RetainOnlyMCPTools filter on ToolStore, and the wiring call in getToolsStoreForUser. Nil = all tools allowed (backward compat), empty = no MCP tools, populated = allowlist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and MCP tools Phase 7: Creates agent container factory, API helper, page object, and three test spec files covering create/edit/delete flows, user access level enforcement, and MCP tool selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Search input on listing page filters by display name and username - Field-level validation errors in config modal with clear-on-edit - Server-side 409 (username taken) mapped to inline field error - Service deleted edge case: warning badge in agent row, fallback option in service dropdown - MCP server removed edge case: orphaned tools warning banner with auto-cleanup on tab load Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ServiceID stores UUIDs (36 chars) but was defined as VARCHAR(26), causing POST /agents to fail with a Postgres truncation error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
navigateToAgentsPage() was pushing /plug/mattermost-ai/agents without the team prefix, causing Mattermost's router to treat it as an unknown team and redirect to "Team Not Found". Now extracts the current team name from the URL path and builds the full team-scoped route. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use registerProduct (internal MM API) instead of registerMainMenuAction so "Agents" appears in the product switcher grid icon rather than the team dropdown menu. Simplify AgentsPage to a normal component — routing is handled by registerProduct, so no URL-matching overlay is needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverts the registerProduct change (internal API not reliable) and
restores the original rootComponent overlay + registerMainMenuAction
approach. Fixes two issues in the original code:
1. navigateToAgentsPage() now extracts the current team name from the
URL and navigates to /${teamName}/plug/mattermost-ai/agents
2. AgentsPage visibility check uses includes() instead of endsWith()
to match team-prefixed URLs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…efix fix" This reverts commit 7f7ba76.
…ation errors Add pre-validation for username format (must match ^[a-z][a-z0-9._-]*$) returning 400 for invalid usernames. Detect duplicate username errors from bot creation and return 409 Conflict instead of 500. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add app__body and channel-view CSS classes on mount in AgentsPage, matching what Playbooks and Boards do in their product components. ChannelController normally sets these classes but isn't loaded in product views, causing the header to render with a white background. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SetEnabledToolsFromJSON was treating "[]" the same as "" (empty string), mapping both to nil. This destroyed the nil-vs-empty distinction: - nil EnabledTools = all tools allowed (config-defined bots) - [] EnabledTools = no tools allowed (user disabled all toggles) When an agent was saved with no MCP tools enabled, the DB stored "[]" but on read it was converted to nil, bypassing the RetainOnlyMCPTools filter in llm_context.go and allowing all tools at runtime. Fix: only map "" (empty DB column from pre-feature agents) to nil. Let json.Unmarshal handle "[]" → empty slice and "null" → nil correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Tools popover in the RHS chat header was showing ALL available MCP tool providers regardless of the selected agent's configured tools. Changes: - Backend: Add EnabledMCPTools to AIBotInfo so /ai_bots exposes each bot's allowed MCP tools to the frontend - Frontend: Add enabledMCPTools to LLMBot interface, pass it to ToolProviderPopover, and filter the server list so only servers with at least one enabled tool are shown - Config-defined bots (null enabledMCPTools) continue to show all tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rness - Register tests/agents/*.spec.ts in e2e-shard-3; validate ci-test-groups - crud: duplicate username text, search no-results, regular user listing, unprivileged 403 UI - access-control: allowlist negative, UserAccessLevel None listing, delegated admin edit - mcp-tools: RHS tool provider cases; longer container beforeAll timeouts - api: RHS MCP filtering + tests; ai-plugin helpers for Tools menu Made-with: Cursor
…rules) - MattermostPage: assert DM outcomes via bot posts (Client4) instead of thread reply UI - access-control: use expectNoBotDmReplyFromApi / expectBotDmReplyFromApi - openai-mock: buildChatCompletionMockRule for ordered body matchers - mcp-tools: layered Smocker rules to tie responses to read_post in completion payload Made-with: Cursor
- expectNoBotDmReplyFromApi: poll until observeDurationMs elapses (default 45s, matches positive reply timeout) - Remove early return after minObserve; extend block/allowlist-negative tests to 120s Made-with: Cursor
Ensure the positive enabled-tools flow only passes after a mocked tool-call round trip, so the suite proves runtime MCP execution instead of just mocked response text. Made-with: Cursor
…migration - Persist model, vision, tools, native tools, reasoning, structured output on user agents (DB, API, runtime bot mapping). - Expose ServiceInfo fields and POST /agents/models/fetch for server-side model listing. - Agents modal Configuration tab: full parity with legacy bot form; MCPs tab disabled when tools off. - Structured output vs extended thinking: mutual exclusion, restore reasoning when structured off, inline note. - Modal scroll fixes (min-height) for long config forms; service selection hint for advanced options. - System Console: replace AI Bots editor with moved notice + link; default bot from getAIBots(). - Idempotent startup migration from config.bots to DB agents; sysadmin can manage migrated agents (empty creator). - Tests: API/store, E2E crud and system console; skip legacy bot UI specs pending agents coverage. - Export NativeToolsItem from bot.tsx for reuse. Made-with: Cursor
Add agent-builder coverage for the provider-specific settings that moved out of the system console, and rerun legacy config-bot migration after later config updates so seeded bots still appear in the UI. Made-with: Cursor
Apply fixes from CodeRabbit review: add username validation on agent update, guard against nil config in service validation, bound avatar upload size (10MB / 413), align SetEnabledNativeToolsFromJSON empty-slice semantics, remove legacy bot-reasoning-config spec (coverage moved to provider-config), add optional chaining in e2e afterAll teardown, and improve webapp accessibility (avatar alt text, dialog ARIA attributes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extend user agent model, API, store, and bots integration - Migrate legacy config bots to user agents on plugin enable - Sync Mattermost bot display name and deactivate on agent update/delete - Update agents UI, system console config, and i18n strings - Add permission helper and e2e agent CRUD coverage - Fix golangci errcheck and shadow issues in agent handlers and migration Made-with: Cursor
Remove .planning/phase-1/PLAN.md from version control (keep local copy). .gitignore: use .planning pattern for the planning directory. Made-with: Cursor
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds DB-backed self-service AI agents with REST endpoints, migrations, bots registry integration and cluster events, per-agent MCP tool allowlists, frontend Agents UI and client APIs, E2E test suites and helpers, and assorted plumbing and dependency updates. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant API
participant Store
participant BotRegistry
participant Cluster
Client->>API: POST /agents (CreateAgentRequest)
API->>Store: CreateAgent(agent)
Store-->>API: created agent (ID)
API->>BotRegistry: create/update Mattermost bot account
BotRegistry-->>API: bot_user_id
API->>Cluster: PublishAgentUpdate()
Cluster->>BotRegistry: ForceRefreshOnNextEnsure() & EnsureBots()
BotRegistry-->>Cluster: EnsureBots result
API-->>Client: 201 Created (agent)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
✨ Finishing Touches🧪 Generate unit tests (beta)
|
…RY Responses API - Skip deactivating plugin-owned Mattermost bots that still map to active DB agents when EnsureBots cannot reconcile them (missing/invalid service). - Centralize ServiceUsesResponsesAPI on llm.ServiceConfig; bifrost and API use it. - refreshBotsAndNotify returns EnsureBots error; update/delete handlers only apply explicit Bot.Patch / UpdateActive when EnsureBots did not run, failed, or no bot registry is configured. GitHub: replied on PR #589 to Cursor auth threads (handlers use canConfigureAgentServices). Made-with: Cursor
|
Test server creation failed. Review the error details here. |
Agent list/get routes call canUserAccessAgent without aiBotRequired; dereferencing a.bots for usage checks could panic when the bot registry is nil. Evaluate restrictions with the plugin client instead; MMBots delegates to the same helper. Made-with: Cursor
|
Test server creation failed. Review the error details here. |
Made-with: Cursor
|
Test server creation failed. Review the error details here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a92efcb. Configure here.
…ls/fetch - Avoid mutating cached Services when normalizing OpenAI UseResponsesAPI for the response. - Use ShouldBindJSON in handleFetchModelsForService for consistent error handling. - Add regression test for GET not mutating stored service config. Made-with: Cursor
|
Test server creation failed. Review the error details here. |
plugin-tests failed on TestAgentUpdate when CreateAgent and UpdateAgent shared the same millisecond: assert.Greater(UpdateAt) failed. Bump UpdateAt to at least previous+1 when the clock has not advanced. Made-with: Cursor
|
Test server creation failed. Review the error details here. |
Update the live service flow to reset agent tool defaults after creation so the DM verification exercises plain replies instead of hanging on pending tool calls. Made-with: Cursor
|
Test server creation failed. Review the error details here. |
Normalize formatting in api_agents.go so the Go lint/style checks pass cleanly. Made-with: Cursor
|
Test server creation failed. Review the error details here. |
|
Test check 0baf37d — action needed Core agent CRUD, store, bots, permissions, bifrost, and config are well-tested across 18 unit and 4 new E2E spec files, but the 137-line legacy bot migration (server/legacy_bot_migration.go) has no unit tests despite complex idempotency, deferred-migration, and all-or-nothing logic.
More details (truncated)Test Files Detected
AnalysisThis is a large feature PR adding full agent CRUD with DB persistence, legacy bot migration, access controls, MCP tool allowlisting, and a rich UI. Test coverage is substantial across most layers: Well-covered areas:
...truncated. View full analysis details Suggestions
To override, comment |
|
/test-analysis-override chimed in at the 11th hour after passing all day. Need to merge this for RC, so will address gaps in follow up PR. |
There was a problem hiding this comment.
Stale comment
Security Review — No Findings
Reviewed all new and modified server-side code (agent CRUD API, store layer, bot registry, permissions, legacy migration, LLM context/tool filtering, configuration, cluster events) and frontend TypeScript for this PR at commit 0baf37d.
Changes since last review (ae6bc1b → 0baf37d): License gating replaced with per-creation quota (
checkAgentCreateQuota), request body size limits added (MaxAgentRequestBodyBytes),BotConfig.Validate()withMaxCustomInstructionsRunescap,sanitizeAgentForUserstripsCustomInstructionsfrom non-admin responses, config clone on GET to prevent shared state mutation,UsageRestrictionsForUserConfigrefactored to standalone function (nil-safe),EnsureBotsprotects active DB agents from deactivation when service is invalid.Key security properties verified:
- Authentication: All agent CRUD endpoints sit behind
MattermostAuthorizationRequiredmiddleware, which reads the server-injectedMattermost-User-Idheader. No identity from request body or query params.- Authorization: Create requires
PermissionManageOwnAgentorPermissionManageSystemplus quota check. Update/delete requirescanManageAgent()(creator, admin list,PermissionManageOthersAgent, orPermissionManageSystemfor ownerless migrated bots). List/get filter bycanUserAccessAgent()with safe default (default: return false). Service/model endpoints requirecanConfigureAgentServices().- Data sanitization:
sanitizeAgentForUser()stripsCustomInstructionsfrom responses for non-manager users, preventing disclosure of sensitive organizational procedures.- Secrets protection:
ServiceInfostruct explicitly excludes API keys, AWS secrets, URLs, and credentials.handleFetchModelsForServiceuses admin-configured credentials server-side viaserviceIDlookup — no user-controlled URLs (no SSRF).- SQL injection: All store queries use parameterized placeholders (
$1–$26).- Input validation: Username regex,
BotConfig.Validate()(field presence, enum range, custom instructions length cap at 16384 runes), service ID existence check against config, username immutability on update, request body size cap (512 KiB), avatar size cap (10 MB viaio.LimitReader).- Immutable fields:
UpdateAgentSQL excludesID,CreatorID,BotUserID,CreateAt,DeleteAt.- MCP tool filtering:
RetainOnlyMCPToolspreserves built-in tools (emptyServerOrigin) and filters MCP tools by per-agent allowlist. Auth errors filtered consistently. Nil allowlist = no filtering (backward compat).- Legacy migration: Cluster mutex, idempotent flag, defers when prerequisites are missing.
- Error handling:
gin.AbortWithErrorstores errors forginloggerbut does not write details to HTTP response bodies.No medium, high, or critical vulnerabilities identified.
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Security Review — PR #589
Scope: Agents CRUD endpoints, permissions model, legacy bot migration, store layer, config exposure, and webapp permissions.
Summary
No medium, high, or critical vulnerabilities found. The PR implements a well-structured authorization model with proper security controls throughout.
Areas Reviewed
Authentication & Identity
- All new agent endpoints sit behind
MattermostAuthorizationRequiredmiddleware, which validates the server-injectedMattermost-User-Idheader. User identity is never taken from request body or query params. ✅
Authorization
- Create: Gated by
PermissionManageOwnAgentorPermissionManageSystemviacanCreateAgent(). - Read (list/get): Filtered by
canUserAccessAgent()which delegates toUsageRestrictionsForUserConfigfor user-scope access checks. - Update/Delete: Gated by
canManageAgent()requiring agent admin status (creator or delegated),PermissionManageOthersAgent, orPermissionManageSystemfor legacy bots. - Services/Models:
canConfigureAgentServices()requires agent management permissions. - Immutable fields (
ID,CreatorID,BotUserID,CreateAt,DeleteAt) are preserved from the database record and not modifiable via the update endpoint. ✅
Data Sanitization
sanitizeAgentForUser()stripsCustomInstructionsfrom agent configs returned to non-admin users, preventing exposure of sensitive organizational procedures.- The
/servicesendpoint returns aServiceInfoprojection that explicitly excludes API keys, AWS credentials, and other secrets. ✅
SQL Injection
- All store queries use parameterized placeholders (
$1,$2, etc.). No string concatenation of user input into SQL. ✅
Input Validation
- Request body size capped at 512 KiB via
MaxBytesReader. - Username validated against
^[a-z][a-z0-9._-]*$regex. ServiceIDvalidated against existing admin-configured services.BotConfig.Validate()enforces field constraints including access level ranges andCustomInstructionslength limits. ✅
Secret Exposure
handleGetConfig(returns full config with API keys) is behind admin-only middleware (PermissionManageSystem).handleFetchModelsForServiceuses stored service credentials server-side and returns only model metadata, never the credentials themselves.- Error messages from external API calls are logged server-side via gin's error handling and do not leak into HTTP response bodies. ✅
SSRF
handleFetchModelsForServiceonly accepts aserviceIDreferencing admin-configured services; users cannot supply arbitrary URLs.- The admin-only
handleFetchModelsendpoint accepts user-supplied URLs but is restricted to system admins. ✅
Migration Safety
migrateLegacyConfigBotsToUserAgentsuses a cluster mutex and idempotency flag to prevent concurrent or duplicate migrations.- Migrated agents have
CreatorID = ""andAutoEnableNewMCPTools = true, preserving pre-existing behavior. ✅
No actionable findings to report.
Sent by Cursor Automation: Find vulnerabilities



Summary
Implements user agent CRUD improvements, migration of legacy config-defined bots into user agents, and synchronization of Mattermost bot accounts when agents are updated or deleted (display name patch, bot deactivation). Updates the agents UI, system console wiring, i18n strings, and adds e2e coverage plus a small permissions helper.
Tools list on RHS is based on allowed tools for selected agent


Vs an agent with no tools enabled
Ticket Link
Jira https://mattermost.atlassian.net/browse/MM-65671
Release Note
Made with Cursor
Summary by CodeRabbit
New Features
Migrations
Tests