# 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.
