docs(spec): Phase 8 — agent API
Design for a bearer-token-authenticated REST API letting an external AI coding agent read every project/card the owner has and write back exactly three things: a comment, subtasks, or a column move. Follows the same auth pattern as the existing CRON_SECRET-protected route. Agent-authored comments are prefixed so they're never mistaken for something the owner typed, mirroring Phase 7's "Logged" tag principle. MCP explicitly deferred to a later, thinner follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
3b869d6e48
commit
1c57d2290d
1 changed files with 175 additions and 0 deletions
175
docs/superpowers/specs/2026-09-25-phase-8-agent-api-design.md
Normal file
175
docs/superpowers/specs/2026-09-25-phase-8-agent-api-design.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Phase 8: Agent API — Design
|
||||
|
||||
## Problem
|
||||
|
||||
The owner wants to point an external AI coding agent (Claude Code, MiniMax,
|
||||
or whichever tool is in front of them at the time) at their own backlog —
|
||||
read what's in the Inbox and on the board, draft an implementation plan for
|
||||
a card, and write that plan back onto the card — without that agent having
|
||||
a browser session or a PocketBase login of its own.
|
||||
|
||||
## Goals
|
||||
|
||||
- A machine-to-machine API an external agent can call with a single static
|
||||
credential, no session, no login flow.
|
||||
- Read access across every client/project the owner has — the agent needs
|
||||
to see what's already in progress, not just one Inbox in isolation.
|
||||
- Write access limited to exactly three actions: comment on a card, add
|
||||
subtasks to a card, move a card to a different column.
|
||||
- An agent-authored comment must be honestly marked as agent-drafted,
|
||||
never indistinguishable from something the owner typed — the same
|
||||
principle Phase 7's "Logged" tag exists for, applied here.
|
||||
- A plain REST API, not MCP, for now — the owner hasn't settled on which
|
||||
agent tool will be the primary consumer, and a REST API is callable by
|
||||
anything with a fetch/curl-capable tool. MCP can be layered on top later
|
||||
as a thin wrapper without redesigning anything here.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No per-client or per-project scoping of what the agent can see. This
|
||||
surface exists for the owner's own tooling; it is inherently
|
||||
full-visibility, the same way the admin pages are.
|
||||
- No OAuth, no multiple API credentials, no per-agent permissions. One
|
||||
token, one trust level — full owner-equivalent access — matching this
|
||||
app's existing single-owner architecture (the same posture as the
|
||||
PocketBase superuser credentials it already holds).
|
||||
- No MCP server in this phase. Explicitly deferred.
|
||||
- No card creation, deletion, or field edits beyond the three write actions
|
||||
listed above. An agent that wants to change a card's title, priority, or
|
||||
description does so by leaving a comment for the owner to act on, not by
|
||||
editing the card directly.
|
||||
|
||||
## Authentication
|
||||
|
||||
A single bearer token, `AGENT_API_TOKEN`, generated once (e.g. `openssl rand
|
||||
-hex 32`) and stored in `.env.local`/Coolify — never in a client bundle,
|
||||
never logged. Checked on every request:
|
||||
|
||||
```
|
||||
Authorization: Bearer <AGENT_API_TOKEN>
|
||||
```
|
||||
|
||||
This is the exact pattern already used by `/api/cron/reconcile` (`CRON_SECRET`)
|
||||
— nothing new is being introduced to this codebase's security model, just a
|
||||
second secret of the same kind for a second machine caller. A request with
|
||||
no token, a wrong token, or the token unset in the environment gets a 401
|
||||
without touching PocketBase.
|
||||
|
||||
This is a full owner-equivalent credential. Anyone holding it can read every
|
||||
client's board and post/move/comment as the owner. It must be treated with
|
||||
the same care as the PocketBase superuser credentials.
|
||||
|
||||
## Routes
|
||||
|
||||
All under `/api/agent/`, all reading and writing through the superuser
|
||||
PocketBase connection (`getAdminPb()`), since this surface has no per-caller
|
||||
identity to scope by — the token itself is the identity, and that identity
|
||||
is "the owner."
|
||||
|
||||
### `GET /api/agent/projects`
|
||||
|
||||
Every project across every client, regardless of status filtering used
|
||||
elsewhere in the app (the agent should be able to see paused/completed
|
||||
projects too, not just active ones — it is planning, not browsing).
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "...", "name": "WFDash Web App", "slug": "wfdash-webapp",
|
||||
"client": { "id": "...", "name": "WF", "slug": "wf" },
|
||||
"kind": "project", "status": "active"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `GET /api/agent/projects/:projectId/cards`
|
||||
|
||||
Every card in the project, any column, sorted by column position then card
|
||||
position — the same order the board itself renders in. An optional
|
||||
`?column=inbox` query parameter filters to the Inbox column only (matching
|
||||
`is_inbox`, not a literal column name, since column names are editable).
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "...", "number": 42, "title": "...", "description": "...",
|
||||
"priority": "medium", "target_date": "", "size_band": "medium",
|
||||
"column": { "id": "...", "name": "Inbox", "is_inbox": true, "is_done": false }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `GET /api/agent/cards/:cardId`
|
||||
|
||||
Everything above for one card, plus:
|
||||
- `subtasks`: existing checklist items (`title`, `done`)
|
||||
- `comments`: existing comments (`author` name, `body`, `created`)
|
||||
- `recentCommits`: the last 10 linked commits' summaries (or raw subject if
|
||||
unsummarised) and dates — enough for the agent to know what work has
|
||||
already touched this card without pulling full commit history
|
||||
|
||||
### `POST /api/agent/cards/:cardId/comments`
|
||||
|
||||
Body: `{ "body": string }`.
|
||||
|
||||
The stored comment is prefixed before being written, so no caller can
|
||||
accidentally or deliberately post something indistinguishable from the
|
||||
owner's own words:
|
||||
|
||||
```
|
||||
🤖 Drafted by your agent:
|
||||
|
||||
<body>
|
||||
```
|
||||
|
||||
`author` is resolved server-side to the app's own owner user record (looked
|
||||
up by `role = "owner"` — there is exactly one, by this app's design) — never
|
||||
taken from the request. The comment renders through the exact same
|
||||
`comments` list every card already has; no new UI work is needed.
|
||||
|
||||
### `POST /api/agent/cards/:cardId/subtasks`
|
||||
|
||||
Body: `{ "titles": string[] }`. Creates one subtask per title, appended
|
||||
after the card's existing subtasks in position order (same fractional
|
||||
spacing every other subtask-creation path in this app uses). Each title is
|
||||
trimmed and capped at the schema's 200-character limit; an empty array or
|
||||
an array of only-empty strings is rejected with 400 rather than silently
|
||||
creating nothing.
|
||||
|
||||
### `PATCH /api/agent/cards/:cardId/move`
|
||||
|
||||
Body: `{ "column": string }` — a column **name** (e.g. `"Backlog"`), not an
|
||||
id, since the agent has no reason to know internal PocketBase ids. Resolved
|
||||
case-insensitively against the card's own project's columns; an unknown
|
||||
name is rejected with 400 listing the project's actual column names, so a
|
||||
caller can self-correct without a separate lookup call.
|
||||
|
||||
The card is appended to the end of the target column (highest existing
|
||||
position in that column, plus the standard spacing constant) — there is no
|
||||
concept of "drop position" for a machine caller, so it always goes last,
|
||||
exactly like a freshly created card does.
|
||||
|
||||
## Error handling
|
||||
|
||||
Every route returns the existing `jsonError(message, status)` /
|
||||
`jsonOk(data)` shape already used by `/api/cron/reconcile` and the webhook
|
||||
routes — no new response envelope invented for this surface. 401 for a
|
||||
missing/wrong token, 404 for an unknown project/card id, 400 for a
|
||||
malformed body or an unresolvable column name.
|
||||
|
||||
## Testing
|
||||
|
||||
Integration tests hit these routes directly with `fetch`, the same pattern
|
||||
used throughout `tests/integration/` — a request with no token or the wrong
|
||||
token must be refused before touching PocketBase; a request with the right
|
||||
token must perform exactly the described read/write and nothing else; the
|
||||
comment-prefixing and column-name resolution get their own specific
|
||||
assertions since they are the two places a subtle bug would be easy to miss
|
||||
(a missing prefix silently defeats the whole "honestly marked" requirement).
|
||||
|
||||
## Out of scope for this spec
|
||||
|
||||
- An MCP server wrapping these routes — a natural, small follow-up once a
|
||||
primary agent tool is settled on, not part of this phase.
|
||||
- Any UI in the app itself for managing or rotating `AGENT_API_TOKEN` — it
|
||||
is set the same way `CRON_SECRET` already is, by hand, in the environment.
|
||||
Loading…
Reference in a new issue