vikunja-agents

git clone https://git.dominat.us/oc/vikunja-agents.git

Public read, push restricted to the owner. On reptar the working copy is /srv/pods/vikunja-agents/, which is also where the quadlets and the Claude Code MCP registration point.

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.

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 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:

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.

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

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

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:

vk watch --project 7 --filter "priority >= 3" -- picoclaw --task {task_id}
S
Description
Harness-agnostic Vikunja tooling for agents: CLI, MCP server, task watcher.
Readme
63 KiB
Languages
Python 100%