Roles are workspace-admin presets that customize the AI agent's system-
prompt persona and, optionally, the model, attached to a chat at creation
time. Examples: a 'Proofreader' that only touches grammar, a 'Fact-
checker' that cites web sources. A role changes ONLY instructions and
( optional ) the model; the toolset stays full, so the security boundary
(CASL via the per-user loopback token) is unchanged.
Backend:
- Migration 20260620T150000-ai-agent-roles: ai_agent_roles table
(workspace-scoped, soft-delete, model_config jsonb) + ai_chats.role_id
(ON DELETE SET NULL).
- AiAgentRoleRepo / AiAgentRolesService / AiAgentRolesController at
/workspace/ai-agent-roles. LIST (picker view) is open to all workspace
members; create/update/delete are admin-only. The picker view omits
instructions and model_config so they never leak to non-admins.
- buildSystemPrompt: optional roleInstructions REPLACES the admin persona
(priority order: role > admin > default). The non-removable
SAFETY_FRAMEWORK is always appended - a role cannot strip it.
- AiChatService.stream: persists roleId on first turn; subsequent turns
read role_id from the chat row, never from the request body. The role's
instructions are applied even if it was later disabled or soft-deleted
(existing chats keep their persona).
- AiService.getChatModel accepts an optional override. Same-driver
overrides reuse the workspace key; cross-driver (openai/gemini) loads
alternate creds from ai_provider_credentials and throws a clean 503 if
they are missing (no silent fallback). Cross-driver ollama is rejected
with a clear message (no per-driver ollama base URL exists yet).
- Controller resolves the role model BEFORE res.hijack so misconfigured
overrides return JSON 503, not a broken stream.
Client:
- New chat picker (Mantine Select) lists enabled roles, default
'Universal assistant' (roleId null). The roleId is sent only when
starting a new chat; existing chats show the role as a fixed badge.
- Role badge in the chat window header and conversation list.
- Settings -> AI: new 'Agent roles' management section mirrors the
external MCP servers UI (add/edit/delete + enable toggle + optional
model override). Form fields: name, emoji, description, instructions,
model override (driver + chatModel), with a reminder that the safety
framework is always appended.
Hardening after review:
- Empty-string roleId coerced to null on both client and server (picker
'Universal assistant' option used to crash the uuid INSERT).
- New-chat insert validates picker-eligibility (enabled + not soft-deleted
+ workspace-scoped); ineligible ids silently fall back to null.
- findByCreator's role JOIN is workspace-scoped and every column ref is
table-qualified (avoids Postgres ambiguous-column errors).
- getChatModelForRole applies the same picker-eligibility gate as stream
on the new-chat path, so model and persona resolve from one source.