# 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: ```json { "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 ``` ```json { "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): ```json { "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:** ```json { "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](teams.md) ยง15.3 for the grant system. Personas use grants to control who can see and use them beyond their base scope.