feat(web,hosted): the automation routes: list, make, change, delete, Run now, and run history (automations 4/7)
Automations 4/7, stacked on #2513: the HTTP API the desktop will use (wired up in 7/7). This is the first PR from which an account can create an automation. Its runs go through the sweep (2/7) and run execution (3/7).
Routes (server/automations-app.ts)
All routes take the account bearer. The store gates every row on the account owning the automation and still having access to its plan, so another account's automation, or one in a workspace the account has left, answers not-found.
| Route | What it does |
|---|---|
GET /api/automations[?planId=] | The account's automations, newest first, or one plan's. Each comes with its latest run. |
POST /api/automations | Create one ā 201 { automation, webhookSecret? } |
GET / PATCH / DELETE /api/automations/{id} | Read, change, or delete one with its history |
POST /api/automations/{id}/run { idempotencyKey } | Run now ā 201 { run }. The same key again ā 200 with the same run; nothing new starts. |
GET /api/automations/{id}/runs | Up to 50 runs, newest first, each with its agent's title and status as the agent's tab reads them |
Validation reuses the existing checks:
- Schedule:
readSchedulefrom@sidecar/hosted/automation-schedule, the same module the desktop will validate with before sending. It must be five-field cron in a known zone, no more often than every 15 minutes. - Model: the catalog check a Start uses (
acceptedAgentChoice). - Plan: it must name a repository (
no-repository). - Request body: a key the request frame doesn't name is
invalid-request, as on every request frame.
Scheduling: the next due time is computed when an automation is created, resumed, or given a new trigger, and cleared while it's paused. Resuming also clears the reason the service paused it.
Webhook secret: minted here (32 random bytes, base64url) and returned once, on the write that created it, as webhookSecret. The store keeps only its SHA-256, and no read returns it. The receiving route is in 6/7.
Run now: records a manual firing under the client's key and acts on it immediately, through the same guards and start as a scheduled run. It reaches eve as the developer, under the request's own bearer.
Supporting changes:
- Wire schemas in
automation-wire.ts: summary, run summary, list/answer/create/change/run bodies. - Path builders in
service-paths.ts(automationPath,automationRunPath,automationRunsPath). - Segment rewrites, regenerated with
functions:rewrites. - Four thin route files.
- A run-history store read that joins the agent.
Tests
apps/web/tests/automations-app.test.ts, over a fake eve, a scripted GitHub, a fixed catalog, and PGlite:
- Create: the next run lands at 09:00 New York time (13:00 or 14:00 UTC). The automation is listed, including by plan. For another account it is absent from the list, and GET, PATCH, DELETE and runs are all
not-found. Renaming keeps the due time; deleting removes it. - Refusals, with nothing created:
* * * * *, an unknown zone, an unknown model, and an extra key areinvalid-request; a plan with no repository isno-repository. - Webhook and pause: the secret is returned once, the database holds exactly its SHA-256, and a read never includes it. An automation the service paused reads with
next_run_atcleared. Resuming it restores the due time and clearspausedReason. - Run now: returns
201with a started run and the agentstarting. The same key returns200with the same run, and only one eve session is opened. The history lists the run.
pnpm check:affected passes. There are no UI changes in this PR, so there is no Mac evidence and verify.sh was not run.
š¤ Generated with Claude Code
Summary by cubic
Adds the automation HTTP API: list and create automations, read, change, and delete one, run one now, and read run history. This is the first PR through which an account can create an automation; its runs use the sweep (2/7) and run execution (3/7) already merged.
All routes take the account bearer. The store gates every row on the account owning the automation and still having access to its plan, so another account's automation, or one on a plan of a workspace the account left, answers not-found.
GET /api/automationslists the account's automations (optionally narrowed to one plan) newest first, each with its newest run;POSTcreates one.GET/PATCH/DELETE /api/automations/{id}read, change, or delete one; a change that resumes or gives a new trigger computes a schedule's due time afresh, a pause clears it, resendingenabledas it stands keeps it, and the owner's own pause or resume clears the reason the service paused it.POST /api/automations/{id}/runwith a UUIDidempotencyKeystarts a run now; the same key answers the same run and starts nothing new.GET /api/automations/{id}/runsanswers up to 50 runs, newest first, each with the agent's title and status.- Validation matches a five-field cron in a known zone, no more often than every 15 minutes, a model the catalog offers, a plan with a repository, and a plan filter that names a real plan; anything else is refused with nothing created.
- Guards and the run claim run under the automation's row lock, so a sweep and a Run now never both read room for one more agent; a claim counts against concurrency from when it was made, and one left without its agent is recovered as the start its key made only where that agent's session ran ā an agent made but never opened is discarded ā or failed as unavailable.
- A webhook trigger's secret is minted here (32 random bytes, base64url), returned once on creation, and kept only as SHA-256; a webhook sent again keeps the secret, and the body bound fits every prompt the schema takes.
- Run-now fires the agent as the developer, under the request's own bearer.
Tests cover creation, listing, refusals, webhook secret handling, pause/resume, run-now idempotency, concurrent acts, and stranded-run recovery over a fake eve, a scripted GitHub, a fixed catalog, and PGlite.