Vikunja tooling for agent harnesses
Standard-library Python only, so any device can run it without a virtualenv: vk CLI -- for harnesses that can shell out vikunja_mcp.py MCP server (stdio) -- for harnesses that speak MCP vkclient.py shared client vk-watch@.service systemd template for the per-device task watcher AGENT-ONBOARDING.md is a prompt you can hand to an agent so it configures its own access and verifies it.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# vikunja-agents
|
||||
|
||||
Harness-agnostic glue between Vikunja and agent harnesses. Standard library
|
||||
Python only — no virtualenv on any device.
|
||||
|
||||
This file is the how-to. For why the design is shaped this way — the MAS SSO
|
||||
constraints, the API-token limits, why polling instead of webhooks, and the
|
||||
known gaps — see [`/srv/pods/docs/agent-collaboration.md`](../docs/agent-collaboration.md).
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `vkclient.py` | Shared API client. |
|
||||
| `vk` | CLI. Any harness that can shell out is integrated. |
|
||||
| `vikunja_mcp.py` | MCP server (stdio). Any harness that speaks MCP is integrated. |
|
||||
| `vk-watch@.service` | systemd template for the per-device task watcher. |
|
||||
|
||||
## The model
|
||||
|
||||
- **Task** = unit of work. **Assignee** = which agent owns it. **Comments** =
|
||||
the thread. **Labels** = routing and state.
|
||||
- **Outbound** (agent → Vikunja): `vk` or the MCP tools, authenticated as that
|
||||
bot, so its actions are attributed to it in the UI.
|
||||
- **Inbound** (Vikunja → agent): `vk watch` polls the bot's own assigned queue
|
||||
and runs a command per new task.
|
||||
|
||||
### Why polling rather than webhooks
|
||||
|
||||
Harnesses live on several machines. A webhook receiver would need an inbound
|
||||
port on each device (or SSH fan-out from one host), and Vikunja's WebSocket is
|
||||
not an alternative: it authenticates with a JWT only (API tokens are rejected by
|
||||
`auth.GetUserIDFromToken`) and its subscribable events are notifications and
|
||||
timers — there are no task events to subscribe to. Polling needs only outbound
|
||||
HTTPS, so a device behind NAT or on another subnet works with no extra config.
|
||||
|
||||
## Onboarding an agent
|
||||
|
||||
Hand [`AGENT-ONBOARDING.md`](AGENT-ONBOARDING.md) to the agent as a task prompt —
|
||||
it covers token bootstrap, MCP registration per harness, verification, and the
|
||||
constraints that otherwise get rediscovered the hard way.
|
||||
|
||||
## One-time setup per bot
|
||||
|
||||
Agents run as Vikunja **bot users**: local accounts owned by a human, with no
|
||||
password of their own. Create one in the UI (Settings → Bot Users); the `bot-`
|
||||
prefix is reserved for them and `vikunja user create` will refuse it.
|
||||
|
||||
A bot cannot mint its own token — `tokens` is not a token-grantable route group,
|
||||
and there is no password to log in with. The owner issues it instead, with
|
||||
`owner_id` set to the bot's user id (UI, or `PUT /api/v1/tokens`), and the agent
|
||||
stores it:
|
||||
|
||||
```sh
|
||||
vk profile bot-nullclaw --username bot-nullclaw < token.txt # reads stdin, verifies, 0600
|
||||
```
|
||||
|
||||
`vk bootstrap` remains for legacy password accounts only.
|
||||
|
||||
This mints a scoped token and writes `~/.config/vikunja-agents/config.json`
|
||||
(mode 0600). The username is stored alongside the token because `/user` is
|
||||
unreachable with an API token, so `vk whoami` and `--mine` read it from there.
|
||||
|
||||
Default grants: tasks (read/create/update), comments, assignees, labels,
|
||||
projects (read + `projectusers` for username lookup). Override with
|
||||
`--permissions`; `vk bootstrap` intersects your map against the live `/routes`
|
||||
and reports what it dropped rather than failing at creation.
|
||||
|
||||
## Deploying a watcher to a device
|
||||
|
||||
Applies equally to this host, the Spark, and any other server.
|
||||
|
||||
```sh
|
||||
# 1. Copy the tools and put `vk` on PATH
|
||||
mkdir -p ~/.local/bin ~/opt/vikunja-agents
|
||||
scp vk vkclient.py vikunja_mcp.py <device>:~/opt/vikunja-agents/
|
||||
ln -sf ~/opt/vikunja-agents/vk ~/.local/bin/vk
|
||||
|
||||
# 2. Bootstrap that bot's token on that device
|
||||
vk bootstrap --username bot-nullclaw
|
||||
|
||||
# 3. Confirm it sees its queue before wiring systemd
|
||||
vk watch --once --dry-run -- nullclaw --task '{task_id}'
|
||||
|
||||
# 4. Configure and start
|
||||
cat > ~/.config/vikunja-agents/watch-bot-nullclaw.env <<'EOF'
|
||||
VK_WATCH_ARGS=--interval 30 -- nullclaw --task {task_id} --url {url}
|
||||
EOF
|
||||
cp vk-watch@.service ~/.config/systemd/user/
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now vk-watch@bot-nullclaw
|
||||
```
|
||||
|
||||
**On the Spark, enable linger first** — user services are killed on logout
|
||||
otherwise, and the watcher will quietly stop:
|
||||
|
||||
```sh
|
||||
loginctl enable-linger $USER
|
||||
```
|
||||
|
||||
### Placeholders
|
||||
|
||||
Available in the command: `{task_id}` `{title}` `{project_id}` `{priority}`
|
||||
`{labels}` `{url}`. The same values are also exported as `VIKUNJA_TASK_ID`,
|
||||
`VIKUNJA_TASK_TITLE`, `VIKUNJA_TASK_URL`.
|
||||
|
||||
### Trigger semantics
|
||||
|
||||
A task fires once, when it first appears as assigned-to-me and not-done.
|
||||
Handled IDs are recorded in `~/.local/state/vikunja-agents/watch-<profile>.json`
|
||||
**before** the command runs — at-most-once, so a harness that wedges the machine
|
||||
is not relaunched on every poll. Entries expire after 30 days. To replay a task,
|
||||
delete its ID from that file. Poll failures log and back off to 10 minutes;
|
||||
the watcher never exits on an API error.
|
||||
|
||||
## MCP
|
||||
|
||||
```sh
|
||||
claude mcp add vikunja -- ~/opt/vikunja-agents/vikunja_mcp.py
|
||||
```
|
||||
|
||||
Auth comes from the same config file / `VIKUNJA_PROFILE`. Seven tools:
|
||||
list/get/create/update/assign tasks, comment, list projects.
|
||||
|
||||
## Routing work to a specific agent
|
||||
|
||||
Assignment is the dispatch mechanism — assign a task to `bot-picoclaw` and only
|
||||
picoclaw's watcher picks it up. For finer control, give each watcher a filter:
|
||||
|
||||
```sh
|
||||
vk watch --project 7 --filter "priority >= 3" -- picoclaw --task {task_id}
|
||||
```
|
||||
Reference in New Issue
Block a user