244 lines
6.8 KiB
Markdown
244 lines
6.8 KiB
Markdown
# 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.
|