6.8 KiB
Personas
A Persona is the unit of access control and AI identity: system prompt + model + provider config + KB bindings + grant scoping. Users interact with Personas, not raw provider configs. Admins curate which Personas are available; team admins create team-scoped Personas.
Three scopes, three route namespaces, one domain:
| Scope | Routes | Who manages |
|---|---|---|
| Personal | GET/POST/PUT/DELETE /personas |
The user |
| Team | GET/POST/PUT/DELETE /teams/:teamId/personas |
Team admin |
| Global (admin) | GET/POST/PUT/DELETE /admin/personas |
Platform admin |
All three return the same Persona object shape:
{
"id": "uuid",
"name": "Code Reviewer",
"handle": "code-reviewer",
"description": "Reviews code for bugs and style",
"icon": "🔍",
"avatar": "data:image/png;base64,...",
"base_model_id": "claude-sonnet-4-20250514",
"provider_config_id": "uuid|null",
"system_prompt": "You are a senior code reviewer...",
"temperature": 0.7,
"max_tokens": 4096,
"thinking_budget": null,
"top_p": null,
"scope": "personal|team|global",
"owner_id": "uuid|null",
"created_by": "uuid",
"is_active": true,
"is_shared": false,
"memory_enabled": false,
"memory_extraction_prompt": null,
"grants": [],
"kb_ids": [],
"created_at": "...",
"updated_at": "..."
}
grants and kb_ids are loaded from junction tables, not stored on the
persona row. All nullable fields (provider_config_id, owner_id,
temperature, max_tokens, thinking_budget, top_p,
memory_extraction_prompt) are omitted from JSON when null.
handle is auto-generated from name on creation if not provided
(e.g. "Code Reviewer" → "code-reviewer"). Used for @mention routing.
Personal Personas
Gated by allow_user_personas policy.
GET /personas → { "data": [...] }
POST /personas ← { "name", "base_model_id", ... }
PUT /personas/:id ← partial update
DELETE /personas/:id
Auth: JWT required. POST requires persona:create permission.
PUT/DELETE require persona:manage permission and ownership check
(scope == "personal" and owner_id == caller).
Team Personas
GET /teams/:teamId/personas → { "data": [...] }
POST /teams/:teamId/personas ← same shape
PUT /teams/:teamId/personas/:id ← partial update
DELETE /teams/:teamId/personas/:id
Auth: JWT required. Team membership for GET, team admin for
POST/PUT/DELETE. All mutating endpoints verify the persona belongs
to the team in the URL path (scope == "team" and owner_id == teamId).
Admin (Global) Personas
GET /admin/personas → { "data": [...] }
POST /admin/personas ← same shape
PUT /admin/personas/:id
DELETE /admin/personas/:id
Auth: Admin JWT required (middleware enforced).
Persona KB Bindings
A Persona can have Knowledge Bases bound to it. When a channel uses
that Persona, the bound KBs are automatically scoped into kb_search
tool calls. Users never see or manage the underlying KBs directly —
they talk to the Persona.
Get bindings:
GET /personas/:id/knowledge-bases → { "data": [KB objects] }
GET /teams/:teamId/personas/:id/knowledge-bases → { "data": [...] }
GET /admin/personas/:id/knowledge-bases → { "data": [...] }
Set bindings (replace all):
PUT /personas/:id/knowledge-bases
PUT /teams/:teamId/personas/:id/knowledge-bases
PUT /admin/personas/:id/knowledge-bases
{
"kb_ids": ["kb-uuid-1", "kb-uuid-2"],
"auto_search": {
"kb-uuid-1": true,
"kb-uuid-2": false
}
}
auto_search: when true, the KB is always searched (prepended to
context). When false, it's available via the kb_search tool but
not auto-injected.
Team-scoped KB binding endpoints verify the persona belongs to the team in the URL path before reading or writing bindings.
Persona Avatars
Avatars are stored as data:image/png;base64,... data URIs in the
avatar column. Upload accepts a JSON body (not multipart):
{ "image": "data:image/png;base64,..." }
The image is decoded, resized to 128×128 PNG, and stored.
POST /personas/:id/avatar ← { "image": "base64..." }
DELETE /personas/:id/avatar
POST /admin/personas/:id/avatar ← same
DELETE /admin/personas/:id/avatar
POST /teams/:teamId/personas/:id/avatar ← same (verify team ownership)
DELETE /teams/:teamId/personas/:id/avatar
Auth: Personal avatar routes verify the caller owns the persona
(scope == "personal" and owner_id == caller). Admin routes are
protected by admin middleware.
Persona Tool Grants
Control which tools a persona can use during completions. The completion handler applies tool grants as a second-pass allowlist.
GET /personas/:id/tool-grants → { "data": ["web_search", "kb_search", ...] }
PUT /personas/:id/tool-grants ← { "tool_names": ["web_search", "calculator"] }
Admin equivalents:
GET /admin/personas/:id/tool-grants
PUT /admin/personas/:id/tool-grants
Team equivalents (verify persona belongs to team):
GET /teams/:teamId/personas/:id/tool-grants
PUT /teams/:teamId/personas/:id/tool-grants
When a workflow version is published, persona tool grants at that moment are frozen into the version snapshot.
Persona Groups
Saved roster templates. A persona group is a named set of personas that can be stamped onto new conversations.
GET /persona-groups → { "data": [...] }
POST /persona-groups ← { "name", "description" }
GET /persona-groups/:id → group with members
PUT /persona-groups/:id ← partial update
DELETE /persona-groups/:id
Members:
POST /persona-groups/:id/members ← { "persona_id", "is_leader": false }
DELETE /persona-groups/:id/members/:memberId
is_leader: the leader persona responds when no @mention is present in
a group channel. Only one leader per group. Setting a new leader
automatically clears the existing one.
Persona Group Object:
{
"id": "uuid",
"name": "Support Team",
"description": "...",
"owner_id": "uuid",
"scope": "personal",
"team_id": null,
"members": [
{
"id": "uuid",
"group_id": "uuid",
"persona_id": "uuid",
"is_leader": true,
"sort_order": 0,
"persona_name": "...",
"persona_handle": "...",
"persona_avatar": "..."
}
],
"created_at": "...",
"updated_at": "..."
}
Note: Persona groups are currently personal-scope only. The scope
and team_id fields exist in the schema but POST hardcodes
scope = 'personal'. Team-scoped persona groups are a future item.
Resource Grants
See teams.md §15.3 for the grant system. Personas use grants to control who can see and use them beyond their base scope.