v0.8.2 — identity binding and capability scoping
Closes the unauthenticated MCP tool path and makes actor / completed_by mean something.
One release covering v0.7.0 → v0.8.2, all of which merged in a single build session. Cutting four tags for one build would be noise, but noting it explicitly rather than letting the intermediate versions disappear — the CHANGELOG has a full entry for each.
v0.7.0 — authenticate the MCP tool path (vikunja#387)
The transport had no auth. Only the seven HTTP control routes were gated; a comment referred to "the MCP auth middleware", but none was ever configured. An unauthenticated initialize returned HTTP 200 with a session id, so any caller could invoke any tool while asserting any actor — including operator, which every ownership check exempts.
Each agent now gets a distinct bearer token via TASK_QUEUE_TOKEN_<AGENT>, so the token both authenticates the caller and identifies it. No separate identity header on purpose: an agent holding a token can set any header it likes on a direct request, so a header-derived identity would be a weaker second channel competing with the token-derived one.
Fails closed on every misconfiguration — no tokens, an empty value, a token under 16 chars, a token shared by two agents, or one minted for the reserved operator identity.
v0.8.0 — bind identity, gate the operator-only tools
actor is derived from the token's sub claim. A mismatch is refused, not silently corrected. source_agent on submit_task is bound too — the auto-close decides whether to fire from it, so binding only actor would have left a route to terminally closing another agent's task without ever calling update_task.
set_task_status and cancel_task become operator-only. park_task/unpark_task get an ownership rule instead, so an agent can pause its own work and nobody else's. actor is pinned to operator on all six control routes rather than defaulted.
Adds POST /tasks/{id}/update — an audited operator sweep with on_behalf_of, verified against the task's real target_agent and recorded alongside it in history. It replaces the dishonest sweep that binding actor removed.
Adds an adversarial suite: every route agent A has to act on agent B's task, each of which must fail, with the honest paths asserted alongside.
v0.8.1 — stop hiding open work
list_tasks TTL-filtered everything except parked, so open work silently vanished from listings while still on disk. That is how a sweep found 17 stranded tasks where this tool reported 13. Non-terminal statuses are now exempt; terminal records still age out.
The auto-close note now carries the return task's summary — it always wins the race against the answering agent's own close, so its note is what history actually records.
v0.8.2 — audit remediation
Single source of truth for the operator identity, which had been spelled independently in three places. Guarded by a source-level test, because the obvious runtime check is vacuous: CPython interns identifier-like literals, so is passes even with two separate definitions.
Deploying
Requires per-agent tokens before the server starts demanding them. A bearer header sent to a server with no auth is ignored, so client rollout is safely additive; only the server restart closes the gate.
Scope
This contains a mistaken or prompt-injected agent acting through its own tool surface, and makes the audit trail mean what it says. It is not a boundary against an agent that goes looking for credentials — that needs per-agent OS users or a credential broker.
Security audit 2026-08-16: 2 findings (1 Medium pre-existing/deployment, 1 Low fixed here). All six flagged judgment calls independently attacked; no bypass found. 209 tests, 92% coverage.