Compare commits
3 Commits
main
...
claude/exp
| Author | SHA1 | Date |
|---|---|---|
|
|
10292af76f | |
|
|
ce7956e842 | |
|
|
55f0322808 |
|
|
@ -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`).
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
# Exploring OpenClaw/NanoClaw Integration with Vikunja
|
||||
|
||||
## What Are OpenClaw and NanoClaw?
|
||||
|
||||
**OpenClaw** is an open-source AI agent framework (the fastest-growing GitHub project in history as of early 2026). It connects to messaging apps and enables LLMs to autonomously perform tasks. It exposes an HTTP Agent API with endpoints like `/api/agent/createTask`, `/api/agent/updateTask`, etc., and has 100+ preconfigured "AgentSkills."
|
||||
|
||||
**NanoClaw** is a lightweight, security-focused alternative built on Anthropic's Claude Agent SDK. It prioritizes containerized isolation and radical simplicity (~500 lines of core code vs OpenClaw's ~500k). It uses a task-scheduler with cron/interval scheduling and isolated container sandboxes per group.
|
||||
|
||||
Both frameworks can connect to external services, execute code, and manage tasks autonomously.
|
||||
|
||||
---
|
||||
|
||||
## Design Principle
|
||||
|
||||
**Assigning an agent to a task should feel exactly like assigning a coworker.**
|
||||
|
||||
- Bot users appear in the same assignee picker as human users
|
||||
- You assign them the same way — search, click, done
|
||||
- Progress shows up as regular comments in the task thread
|
||||
- The task gets marked done when the bot finishes
|
||||
- The only visual difference is a small bot badge next to the name
|
||||
|
||||
No separate "agent panels," no special buttons. The existing assignee UX *is* the interface.
|
||||
|
||||
---
|
||||
|
||||
## Integration Concept
|
||||
|
||||
The core idea: **assign an AI agent (via OpenClaw or NanoClaw) to a Vikunja task, and the agent autonomously works to complete it.**
|
||||
|
||||
This requires two directions of communication:
|
||||
|
||||
1. **Vikunja → Agent**: Assigning a bot user to a task triggers a dispatch to the agent's endpoint
|
||||
2. **Agent → Vikunja**: The agent calls back via the standard Vikunja API to post comments, update the task, attach files, and mark it done
|
||||
|
||||
---
|
||||
|
||||
## What Already Exists in Vikunja That Helps
|
||||
|
||||
### Task Assignees (the primary UX surface)
|
||||
- Tasks can have multiple assignees (users)
|
||||
- Assignment dispatches `TaskAssigneeCreatedEvent`
|
||||
- Existing assignee picker UI with search
|
||||
- **This becomes the integration trigger** — assigning a bot user dispatches work to the agent
|
||||
|
||||
### REST API + API Tokens (inbound control)
|
||||
- Full CRUD API for tasks, comments, attachments, assignees
|
||||
- Scoped API tokens (`tk_` prefix) with per-route permissions
|
||||
- **This handles Agent → Vikunja updates** — the bot user gets its own API token
|
||||
|
||||
### Event System (Watermill-based)
|
||||
- Async event dispatch with retry logic
|
||||
- Existing task lifecycle events (created, updated, deleted, assignee changes)
|
||||
- Listener registration pattern for extending behavior
|
||||
- **New listener on `TaskAssigneeCreatedEvent`** checks if assignee is a bot and dispatches
|
||||
|
||||
### Cron System
|
||||
- `robfig/cron/v3` for scheduled background work
|
||||
- Used for reminders, overdue checks, cleanup jobs
|
||||
- **Can monitor running agent tasks** for timeouts/failures
|
||||
|
||||
### Webhooks (outbound notifications)
|
||||
- Project-level and user-level webhooks already exist
|
||||
- HMAC-SHA256 signing, Basic Auth support
|
||||
- Could supplement the integration but not the primary mechanism
|
||||
|
||||
---
|
||||
|
||||
## What Would Need to Change
|
||||
|
||||
### 1. Bot User Identity (`pkg/user/user.go`)
|
||||
|
||||
Add `IsBot` flag to the User struct:
|
||||
```go
|
||||
IsBot bool `xorm:"bool default false" json:"is_bot"`
|
||||
```
|
||||
|
||||
Bot users are real users with a special flag. They:
|
||||
- Appear in the assignee picker alongside regular users
|
||||
- Show a bot badge in the UI (avatar, comments, assignee lists)
|
||||
- Cannot log in via the web UI — API token only
|
||||
- Don't receive email notifications (reminders, overdue, etc.)
|
||||
- Don't need email confirmation, password, or TOTP
|
||||
- Have their own audit trail — every action is attributed to the bot
|
||||
|
||||
**Database migration:** Add `is_bot` column to `users` table.
|
||||
|
||||
### 2. Agent Connection Config on Bot Users (`pkg/user/user.go` or new model)
|
||||
|
||||
Bot users need agent connection details. Two approaches:
|
||||
|
||||
**Option A: Fields on the User model** (simpler)
|
||||
```go
|
||||
AgentEndpointURL string `xorm:"text null" json:"agent_endpoint_url,omitempty"`
|
||||
AgentAPIKey string `xorm:"text null" json:"-"` // never exposed in API responses
|
||||
AgentType string `xorm:"varchar(50) null" json:"agent_type,omitempty"` // "openclaw", "nanoclaw"
|
||||
```
|
||||
|
||||
**Option B: Separate `bot_configs` table** (cleaner separation)
|
||||
```go
|
||||
type BotConfig struct {
|
||||
ID int64
|
||||
UserID int64 // FK to users, unique (one config per bot user)
|
||||
EndpointURL string
|
||||
APIKey string // encrypted at rest
|
||||
Type string // "openclaw", "nanoclaw"
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
```
|
||||
|
||||
**Recommendation: Option B** — keeps the User model clean, allows the config to be managed independently, and makes it easier to encrypt the API key at rest.
|
||||
|
||||
### 3. Agent Dispatch Listener (`pkg/models/listeners.go`)
|
||||
|
||||
New listener on `TaskAssigneeCreatedEvent`:
|
||||
|
||||
```
|
||||
When a bot user is assigned to a task:
|
||||
1. Load the bot's agent config (endpoint URL, API key, type)
|
||||
2. Load full task context (title, description, comments, attachments, related tasks)
|
||||
3. Generate a scoped API token for the bot user (if one doesn't exist)
|
||||
4. POST to the agent endpoint with task details + callback token
|
||||
5. The agent starts working autonomously
|
||||
```
|
||||
|
||||
Corresponding listener on `TaskAssigneeDeletedEvent`:
|
||||
```
|
||||
When a bot user is unassigned from a task:
|
||||
1. POST a cancellation request to the agent endpoint
|
||||
2. Agent stops work on that task
|
||||
```
|
||||
|
||||
### 4. Agent Adapter Interface (`pkg/modules/agents/`)
|
||||
|
||||
Generic interface with provider-specific implementations:
|
||||
|
||||
```go
|
||||
type AgentProvider interface {
|
||||
// Dispatch sends a task to the agent for processing
|
||||
Dispatch(ctx context.Context, task *models.Task, callbackURL string, callbackToken string) error
|
||||
// Cancel tells the agent to stop working on a task
|
||||
Cancel(ctx context.Context, taskID int64) error
|
||||
// Status checks if the agent is healthy
|
||||
Status(ctx context.Context) (AgentStatus, error)
|
||||
}
|
||||
```
|
||||
|
||||
Implementations:
|
||||
- `pkg/modules/agents/openclaw/` — OpenClaw HTTP API adapter
|
||||
- `pkg/modules/agents/nanoclaw/` — NanoClaw adapter
|
||||
|
||||
### 5. Bot User Creation API
|
||||
|
||||
New endpoint or flag on existing user creation:
|
||||
|
||||
- `PUT /api/v1/bots` — create a bot user (admin or project admin only)
|
||||
- Takes: name, username, agent type, endpoint URL, API key
|
||||
- Creates user with `is_bot: true`
|
||||
- Creates associated `BotConfig`
|
||||
- Auto-generates an API token for the bot
|
||||
- Returns the bot user + token (token shown only once)
|
||||
|
||||
- `GET /api/v1/bots` — list bot users accessible to current user
|
||||
- `POST /api/v1/bots/{id}` — update bot config
|
||||
- `DELETE /api/v1/bots/{id}` — deactivate bot user
|
||||
|
||||
### 6. Frontend Changes
|
||||
|
||||
**Minimal — that's the point.** The UX piggybacks on existing patterns:
|
||||
|
||||
#### Assignee Picker (modify existing)
|
||||
- Bot users already appear in the user list (they're real users)
|
||||
- Add a small bot icon/badge next to bot user names
|
||||
- No other changes needed — assignment works identically
|
||||
|
||||
#### User/Avatar Display (`frontend/src/components/misc/User.vue`)
|
||||
- Show bot badge (small robot icon) on avatar
|
||||
- Applied everywhere users are displayed: assignee lists, comments, activity log
|
||||
|
||||
#### Task Comments
|
||||
- Comments from bot users get a subtle "bot" indicator
|
||||
- No separate rendering — same comment thread, same layout
|
||||
- Users reply to bot comments naturally (the agent sees replies via API polling or webhooks)
|
||||
|
||||
#### Bot Management UI (new, minimal)
|
||||
- Settings page for creating/managing bot users
|
||||
- Form: name, username, agent type, endpoint URL, API key
|
||||
- Shows the generated API token once on creation
|
||||
- Could live under team/workspace settings
|
||||
|
||||
#### Model Types (`frontend/src/modelTypes/IUser.ts`)
|
||||
- Add `isBot: boolean` to `IUser`
|
||||
- Add `agentType?: string` to `IUser`
|
||||
|
||||
### 7. Guard Rails & Edge Cases
|
||||
|
||||
**Authentication:**
|
||||
- Bot users cannot authenticate via username/password or OIDC
|
||||
- Only API token auth is valid for bots
|
||||
- Login endpoint rejects users with `is_bot: true`
|
||||
|
||||
**Notifications:**
|
||||
- Skip email notifications for bot users (reminders, overdue, mentions)
|
||||
- Bot users shouldn't trigger "user mentioned" notifications when they @-mention someone? Or should they? (configurable)
|
||||
|
||||
**Permissions:**
|
||||
- Bot users need project access just like regular users (added to project/team)
|
||||
- Bot actions are scoped by the same permission system
|
||||
- Bot can only modify tasks in projects it has write access to
|
||||
|
||||
**Rate Limiting:**
|
||||
- Consider rate limits on bot API calls to prevent runaway agents
|
||||
- Configurable per bot or globally
|
||||
|
||||
**Failure Handling:**
|
||||
- Cron job checks for stalled agent tasks (assigned to bot, no activity for X minutes)
|
||||
- Notify the user who assigned the bot if the agent appears stuck
|
||||
- Auto-unassign after configurable timeout
|
||||
|
||||
---
|
||||
|
||||
## The User Experience, End to End
|
||||
|
||||
### Setup (one-time)
|
||||
1. Admin goes to Settings → Bots
|
||||
2. Creates a bot: "CodeReview Bot", type: OpenClaw, endpoint: `https://openclaw.example.com/api/agent`
|
||||
3. Vikunja creates a bot user, generates an API token, shows it once
|
||||
4. Admin adds the bot user to the relevant project(s)/team(s)
|
||||
|
||||
### Daily Use
|
||||
1. User creates a task: "Review PR #42 for security issues"
|
||||
2. User clicks the assignee picker, sees both coworkers and bots
|
||||
3. User assigns "CodeReview Bot" — looks just like assigning a coworker
|
||||
4. Behind the scenes: Vikunja dispatches the task to OpenClaw
|
||||
5. Minutes later, comments start appearing on the task from the bot:
|
||||
- "Started reviewing PR #42..."
|
||||
- "Found 2 potential issues: SQL injection in `user_query.go:45`, missing auth check in `api/handler.go:112`"
|
||||
- "Full report attached."
|
||||
6. Bot attaches a detailed report file
|
||||
7. Bot marks the task as done
|
||||
8. User sees it all in the normal task detail view — no special UI needed
|
||||
|
||||
### Collaboration
|
||||
- User can reply to bot comments with clarifications
|
||||
- User can unassign the bot to stop it
|
||||
- User can assign a different bot or a human to take over
|
||||
- Multiple bots can be assigned (one researches, another implements)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Bot User Foundation
|
||||
- Add `is_bot` field to User model + migration
|
||||
- Add `BotConfig` model + migration
|
||||
- Bot creation API endpoint
|
||||
- Skip email/password/TOTP requirements for bots
|
||||
- Block web login for bots
|
||||
- Skip email notifications for bots
|
||||
- Frontend: `isBot` on IUser, bot badge on User.vue
|
||||
|
||||
**~8-12 files changed**
|
||||
|
||||
### Phase 2: Agent Dispatch
|
||||
- Agent provider interface (`pkg/modules/agents/`)
|
||||
- OpenClaw adapter
|
||||
- NanoClaw adapter
|
||||
- Listener on `TaskAssigneeCreatedEvent` to dispatch to agent
|
||||
- Listener on `TaskAssigneeDeletedEvent` to cancel agent work
|
||||
- Scoped API token auto-generation for bots
|
||||
|
||||
**~6-10 new files**
|
||||
|
||||
### Phase 3: Monitoring & Resilience
|
||||
- Cron job for stalled agent detection
|
||||
- Notify assigning user on agent failure
|
||||
- Agent health check endpoint
|
||||
- Rate limiting for bot API calls
|
||||
|
||||
**~3-5 files**
|
||||
|
||||
### Phase 4: Polish
|
||||
- Bot management UI in frontend settings
|
||||
- Bot comment styling
|
||||
- Configuration for agent dispatch behavior (what context to send, timeouts)
|
||||
- Documentation
|
||||
|
||||
**~4-6 files**
|
||||
|
||||
---
|
||||
|
||||
## Suggested File Structure
|
||||
|
||||
```
|
||||
pkg/
|
||||
├── user/
|
||||
│ └── user.go # + IsBot field
|
||||
├── models/
|
||||
│ ├── bot_config.go # BotConfig model (endpoint, API key, type)
|
||||
│ ├── bot_config_permissions.go # BotConfig permissions
|
||||
│ ├── events.go # + BotAssignedToTaskEvent
|
||||
│ └── listeners.go # + bot dispatch listener
|
||||
├── modules/
|
||||
│ └── agents/
|
||||
│ ├── agent.go # AgentProvider interface
|
||||
│ ├── dispatch.go # Dispatch logic (called by listener)
|
||||
│ ├── openclaw/
|
||||
│ │ └── openclaw.go # OpenClaw adapter
|
||||
│ └── nanoclaw/
|
||||
│ └── nanoclaw.go # NanoClaw adapter
|
||||
├── routes/
|
||||
│ └── api/v1/
|
||||
│ └── bots.go # Bot CRUD endpoints
|
||||
|
||||
frontend/src/
|
||||
├── modelTypes/
|
||||
│ └── IUser.ts # + isBot, agentType
|
||||
├── models/
|
||||
│ └── user.ts # + isBot default
|
||||
├── components/
|
||||
│ └── misc/
|
||||
│ └── User.vue # + bot badge
|
||||
├── views/
|
||||
│ └── settings/
|
||||
│ └── Bots.vue # Bot management page
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Recommendation | Rationale |
|
||||
|----------|---------------|-----------|
|
||||
| Bot identity | `is_bot` flag on User | Bots are first-class citizens, appear in all user contexts naturally |
|
||||
| Agent config storage | Separate `BotConfig` table | Clean separation, easier to encrypt API keys |
|
||||
| UX surface | Existing assignee picker | "Assign work to a coworker" feeling — no new concepts to learn |
|
||||
| Dispatch trigger | `TaskAssigneeCreatedEvent` listener | Piggybacks on existing event system, zero new UI needed |
|
||||
| Agent communication | Standard Vikunja REST API | Bot uses same API as any other client — comments, updates, attachments |
|
||||
| Provider support | Adapter pattern with interface | Support OpenClaw and NanoClaw (and future providers) cleanly |
|
||||
| Progress reporting | Task comments | Shows up naturally in the existing task detail view |
|
||||
| Completion | Bot marks task done via API | Same as a human marking it done — no special mechanism |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The integration is built on one core insight: **bot users are users.** By adding an `is_bot` flag and wiring up agent dispatch on assignment, the entire existing Vikunja UX — assignee picker, comments, task status, attachments — becomes the agent interface. No new UI paradigms needed.
|
||||
|
||||
The new code is focused on:
|
||||
1. Bot user identity (`is_bot` flag + `BotConfig`)
|
||||
2. Agent dispatch (listener + provider adapters)
|
||||
3. Visual distinction (bot badge in frontend)
|
||||
|
||||
Everything else — permissions, API, events, comments, task lifecycle — already works.
|
||||
Loading…
Reference in New Issue