docs: add detailed design document for bot users feature

Covers data model (is_bot flag + bot_owners table), API endpoints,
auth guardrails, notification skipping, frontend badge, and config flag.

https://claude.ai/code/session_01CL89RAEskas7Hz1TSr7eKR
This commit is contained in:
Claude 2026-03-22 14:10:51 +00:00
parent ce7956e842
commit 10292af76f
No known key found for this signature in database
1 changed files with 477 additions and 0 deletions

477
plans/design-bot-users.md Normal file
View File

@ -0,0 +1,477 @@
# Design: Bot Users
## Overview
Bot users are first-class users with an `is_bot` flag. They appear alongside human users in assignee pickers, comment threads, and everywhere else users show up. The only visual difference is a small "Bot" badge next to their name.
Bot users are always owned by a human user who created them. They cannot log in via username/password — they only interact via the API using tokens. The owning user manages the bot's API tokens.
The feature is gated behind a config flag `service.enablebotusers` (default: `false`).
---
## Data Model
### User table changes
Add one column to the existing `users` table:
```go
// In pkg/user/user.go, add to User struct:
IsBot bool `xorm:"bool default false index" json:"is_bot"`
```
This is the only schema change to `users`. Bot users are regular rows in the `users` table with `is_bot = true`.
Bot users:
- Have a `username` (required, unique as usual)
- Have a `name` (optional display name)
- Have **no password** (empty string, never hashed)
- Have **no email** (empty string — no notifications needed)
- Use `Issuer = "local"` — but we skip the password/email validation for bots (see below)
- Have `Status = StatusActive`
- Have `EmailRemindersEnabled = false`
- Have `OverdueTasksRemindersEnabled = false`
### New table: `bot_owners`
Tracks which human user owns which bot:
```go
// New file: pkg/models/bot_users.go
type BotOwner struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
BotID int64 `xorm:"bigint not null unique index" json:"bot_id"` // FK → users.id (the bot)
OwnerID int64 `xorm:"bigint not null index" json:"owner_id"` // FK → users.id (the human)
Created time.Time `xorm:"created not null" json:"created"`
}
func (*BotOwner) TableName() string {
return "bot_owners"
}
```
**Why a separate table instead of a field on `User`?**
- Keeps the User struct clean — most code doesn't need to know about bot ownership
- Allows querying "all bots owned by user X" efficiently
- Avoids a self-referential FK on the users table
---
## Config
Add to `pkg/config/config.go`:
```go
ServiceEnableBotUsers Key = `service.enablebotusers`
```
Default: `false`. Set in `InitDefaultConfig()`:
```go
ServiceEnableBotUsers.setDefault(false)
```
Add to `config-raw.json` under the `service` section:
```json
{
"key": "enablebotusers",
"default_value": "false",
"comment": "If enabled, users can create bot users that interact with Vikunja via the API only."
}
```
Expose in the `/info` endpoint so the frontend knows whether to show bot-related UI:
```go
// In vikunjaInfos struct:
BotUsersEnabled bool `json:"bot_users_enabled"`
// In Info handler:
info.BotUsersEnabled = config.ServiceEnableBotUsers.GetBool()
```
---
## API Endpoints
All bot endpoints require `service.enablebotusers` to be `true`. Return `403` (or a specific error) if disabled.
### Create Bot
**`PUT /api/v1/user/bots`**
Request body:
```json
{
"username": "my-review-bot",
"name": "Code Review Bot"
}
```
Handler logic:
1. Check `config.ServiceEnableBotUsers.GetBool()` — return error if disabled
2. Validate username (same rules: 1-250 chars, no spaces, not reserved)
3. Create user with `IsBot = true`, no password, no email, `Status = Active`
4. Skip email confirmation flow entirely
5. Don't create an initial project for the bot (bots get added to projects manually)
6. Insert `BotOwner{BotID: newUser.ID, OwnerID: currentUser.ID}`
7. Return the created bot user
Response:
```json
{
"id": 42,
"username": "my-review-bot",
"name": "Code Review Bot",
"is_bot": true,
"created": "2026-03-22T...",
"updated": "2026-03-22T..."
}
```
### List My Bots
**`GET /api/v1/user/bots`**
Returns all bots owned by the current user. Supports search via `?s=` query param.
Handler logic:
1. Query `bot_owners WHERE owner_id = currentUser.ID`
2. Join with `users` to get bot details
3. Return list of bot users
### Get Bot
**`GET /api/v1/user/bots/:bot`**
Returns a single bot by ID. Must be owned by current user.
### Update Bot
**`POST /api/v1/user/bots/:bot`**
Update bot's `name` or `username`. Must be owned by current user.
Request body:
```json
{
"name": "New Bot Name"
}
```
### Delete Bot
**`DELETE /api/v1/user/bots/:bot`**
Soft-deletes (sets `Status = StatusDisabled`) or hard-deletes the bot user. Must be owned by current user.
Considerations:
- What happens to tasks the bot is assigned to? → Unassign the bot from all tasks on deletion.
- What about comments the bot posted? → Keep them (same as when a human user is deleted).
### Create API Token for Bot
**`PUT /api/v1/user/bots/:bot/tokens`**
Creates an API token owned by the bot user, but only the owning human can call this endpoint.
Handler logic:
1. Verify current user owns this bot (check `bot_owners`)
2. Create an `APIToken` with `OwnerID = bot.ID`
3. Return the token (visible only once, same as regular token creation)
Request body:
```json
{
"title": "Main bot token",
"permissions": {
"tasks": ["read_all", "update"],
"task_comments": ["read_all", "create"]
},
"expires_at": "2027-01-01T00:00:00Z"
}
```
### List Bot's API Tokens
**`GET /api/v1/user/bots/:bot/tokens`**
Lists API tokens belonging to the bot. Only the owning human can view these.
### Delete Bot's API Token
**`DELETE /api/v1/user/bots/:bot/tokens/:token`**
Deletes an API token belonging to the bot. Only the owning human can call this.
---
## Auth Changes
### Block password login for bots
In `pkg/routes/api/v1/login.go`, after retrieving the user:
```go
if user.IsBot {
return &user2.ErrAccountIsBot{UserID: user.ID}
}
```
Also in `CheckUserCredentials` (`pkg/user/user.go`) — bots have no password, so `bcrypt.CompareHashAndPassword` would fail naturally. But adding an explicit check is clearer and avoids the timing cost.
### Block CalDAV auth for bots
In `pkg/routes/caldav/auth.go`, reject bot users.
### API token auth works as-is
The existing API token middleware (`pkg/routes/api_tokens.go`) resolves the token owner and puts them in context. If the token belongs to a bot user, the bot user becomes the authenticated user. No changes needed here — the bot acts as itself.
### User creation validation bypass for bots
`checkIfUserIsValid()` in `pkg/user/user_create.go` currently requires password + username for local users and email for all. For bots:
- Skip password requirement
- Skip email requirement
- Still require username
This could be handled by either:
- **Option A**: Adding a bot-specific code path in `checkIfUserIsValid()` that checks `IsBot`
- **Option B**: Creating a separate `CreateBotUser()` function that skips those checks
**Recommendation: Option B** — a separate `CreateBotUser()` function in `pkg/user/` that handles bot-specific creation logic cleanly without polluting the existing user creation path. It would:
1. Validate username only
2. Check uniqueness (same as regular users)
3. Set `IsBot = true`, `Status = Active`, `EmailRemindersEnabled = false`, `OverdueTasksRemindersEnabled = false`
4. Insert user
5. Skip email confirmation, skip initial project creation
6. Dispatch `CreatedEvent` as usual
---
## Notification Changes
### Skip all email notifications for bots
In `User.ShouldNotify()` (`pkg/user/user.go`):
```go
func (u *User) ShouldNotify(sessions ...*xorm.Session) (bool, error) {
// ... existing session setup ...
user, err := getUser(s, &User{ID: u.ID}, true)
if err != nil {
return false, err
}
if user.IsBot {
return false, nil
}
return user.Status != StatusDisabled && user.Status != StatusAccountLocked, err
}
```
This is comprehensive — it prevents all notification types (email, overdue reminders, etc.) for bot users.
---
## User Search / Assignee Picker Changes
### Bots appear in user search
The `ListUsers()` function in `pkg/user/users_project.go` searches by username/name. Bot users will naturally appear in search results since they're regular users. No backend changes needed.
### Bots appear in project member lists
Bot users can be added to projects via the existing project user/team mechanisms. Once added, they appear in the assignee picker for that project's tasks. No changes needed to the project membership system.
---
## Frontend Changes
### IUser type
Add `isBot` to `frontend/src/modelTypes/IUser.ts`:
```typescript
export interface IUser extends IAbstract {
// ... existing fields ...
isBot: boolean
}
```
### User model
Add default in `frontend/src/models/user.ts`:
```typescript
isBot = false
```
### User.vue component — Bot badge
Add a "Bot" badge next to the username when `user.isBot` is true:
```vue
<template>
<div class="user" :class="{'is-inline': isInline}">
<img
v-tooltip="displayName"
:height="avatarSize"
:src="avatarSrc"
:width="avatarSize"
:alt="'Avatar of ' + displayName"
class="avatar"
>
<span v-if="showUsername" class="username">{{ displayName }}</span>
<span v-if="user.isBot" class="bot-badge">Bot</span>
</div>
</template>
```
Style the badge as a small pill/tag:
```scss
.bot-badge {
display: inline-flex;
align-items: center;
font-size: 0.65rem;
font-weight: 600;
padding: 0.1rem 0.35rem;
border-radius: 0.25rem;
background: var(--grey-200);
color: var(--grey-600);
margin-inline-start: 0.25rem;
vertical-align: middle;
text-transform: uppercase;
letter-spacing: 0.02em;
}
```
This badge appears everywhere the `User` component is used: assignee lists, comment headers, task detail sidebar, etc.
### Info store
Add `botUsersEnabled` to the frontend's info/config store so components know whether to show bot-related UI (e.g., the "Manage Bots" settings page).
### Bot management settings page
New page at `frontend/src/views/user/settings/Bots.vue`:
- List bots owned by current user
- Create new bot (form: username, name)
- Edit bot (update name)
- Delete bot (with confirmation)
- Manage bot API tokens:
- Create token (show once, copy to clipboard)
- List existing tokens
- Delete token
This page only appears in the settings sidebar when `botUsersEnabled` is `true`.
---
## Database Migration
Single migration adding:
1. `is_bot` column to `users` table (bool, default false, indexed)
2. `bot_owners` table
```go
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "<timestamp>",
Description: "Add bot user support",
Migrate: func(tx *xorm.Engine) error {
// 1. Add is_bot column
err := tx.Exec("ALTER TABLE users ADD COLUMN is_bot BOOLEAN NOT NULL DEFAULT FALSE")
if err != nil { return err }
err = tx.Exec("CREATE INDEX IDX_users_is_bot ON users (is_bot)")
if err != nil { return err }
// 2. Create bot_owners table
type BotOwner struct {
ID int64 `xorm:"bigint autoincr not null unique pk"`
BotID int64 `xorm:"bigint not null unique index"`
OwnerID int64 `xorm:"bigint not null index"`
Created time.Time `xorm:"created not null"`
}
return tx.Sync2(&BotOwner{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}
```
---
## Error Types
New error types in `pkg/user/error.go` (or `pkg/models/error.go`):
| Error | Code | When |
|-------|------|------|
| `ErrAccountIsBot` | TBD | Bot tries to log in with password |
| `ErrBotUsersDisabled` | TBD | Bot endpoint called when feature is off |
| `ErrBotNotOwned` | TBD | User tries to manage a bot they don't own |
| `ErrCannotMakeBotOwner` | TBD | Trying to set a bot as owner of another bot |
---
## What's NOT in scope (for now)
These are deferred to the agent dispatch phase:
- Agent endpoint configuration (OpenClaw/NanoClaw connection details)
- Auto-dispatching work when a bot is assigned to a task
- Agent status tracking
- Health monitoring / timeout cron jobs
- Bot-specific avatar generation (bots use the default avatar provider for now)
---
## Files to Create or Modify
### New Files
| File | Purpose |
|------|---------|
| `pkg/models/bot_users.go` | BotOwner model, bot CRUD logic |
| `pkg/models/bot_users_permissions.go` | Permission checks for bot operations |
| `pkg/routes/api/v1/bots.go` | Bot API endpoint handlers |
| `pkg/migration/<timestamp>.go` | Database migration |
| `frontend/src/views/user/settings/Bots.vue` | Bot management UI |
| `frontend/src/services/botUser.ts` | Bot API service |
### Modified Files
| File | Change |
|------|--------|
| `pkg/user/user.go` | Add `IsBot` field to `User` struct |
| `pkg/user/user.go` | Update `ShouldNotify()` to return false for bots |
| `pkg/user/user_create.go` | Add `CreateBotUser()` function |
| `pkg/config/config.go` | Add `ServiceEnableBotUsers` config key |
| `config-raw.json` | Add config schema entry |
| `pkg/routes/api/v1/info.go` | Expose `BotUsersEnabled` in `/info` |
| `pkg/routes/api/v1/login.go` | Block bot login |
| `pkg/routes/routes.go` | Register bot API routes |
| `frontend/src/modelTypes/IUser.ts` | Add `isBot` field |
| `frontend/src/models/user.ts` | Add `isBot` default |
| `frontend/src/components/misc/User.vue` | Add bot badge |
| `frontend/src/i18n/lang/en.json` | Add translation strings |
---
## Open Questions
1. **Should bots be sharable?** Can user A transfer ownership of a bot to user B? Or share management access? For now: no. One owner per bot, non-transferable.
2. **Hard delete vs soft delete?** When a bot is deleted, should we hard-delete the user row or set `Status = Disabled`? Soft delete (disabled) is safer since it preserves audit trail and comment attribution. Recommend soft delete.
3. **Bot-to-bot ownership?** A bot should not be able to own another bot. The owner must be a human user (`is_bot = false`). Enforce in the creation handler.
4. **Bot API token scopes — should we restrict?** Should bots be able to have tokens with any permission, or only a subset? For now: same as regular users — any valid permission scope. The owning user decides what the bot can do.
5. **Rate limiting for bot API calls?** Not in this phase. Can be added as a config option later (`service.botratelimit`).