feat(web,hosted): the automation routes: list, make, change, delete, Run now, and run history (automations 4/7)

Merged
DA
7d ago
+1687-58

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.

RouteWhat it does
GET /api/automations[?planId=]The account's automations, newest first, or one plan's. Each comes with its latest run.
POST /api/automationsCreate 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}/runsUp 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: readSchedule from @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 are invalid-request; a plan with no repository is no-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_at cleared. Resuming it restores the due time and clears pausedReason.
  • Run now: returns 201 with a started run and the agent starting. The same key returns 200 with 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


Open workspace in Conductor


Devin Review


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/automations lists the account's automations (optionally narrowed to one plan) newest first, each with its newest run; POST creates 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, resending enabled as it stands keeps it, and the owner's own pause or resume clears the reason the service paused it.
  • POST /api/automations/{id}/run with a UUID idempotencyKey starts a run now; the same key answers the same run and starts nothing new.
  • GET /api/automations/{id}/runs answers 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.