foldrun 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/bin/foldrun.mjs +77 -30
- package/docs/agents.md +66 -5
- package/docs/api-usage.md +20 -5
- package/docs/api.md +33 -16
- package/docs/backups-and-restore.md +35 -66
- package/docs/budgets-and-billing.md +5 -1
- package/docs/cli.md +45 -17
- package/docs/coding-agents.md +9 -3
- package/docs/deploying.md +5 -1
- package/docs/faq.md +5 -2
- package/docs/feature-flags.md +63 -0
- package/docs/flows.md +24 -5
- package/docs/hooks.md +139 -0
- package/docs/notifications.md +7 -1
- package/docs/providers.md +28 -0
- package/docs/runs.md +7 -2
- package/docs/security.md +14 -0
- package/docs/tools.md +46 -6
- package/docs/versions.md +64 -2
- package/docs/workspaces.md +87 -0
- package/package.json +4 -4
- package/src/commands.mjs +1206 -39
- package/src/guide.mjs +1 -1
- package/src/terminal.mjs +85 -0
package/docs/api.md
CHANGED
|
@@ -16,8 +16,8 @@ refuses — there is no separate "browser API". The exceptions carry their own
|
|
|
16
16
|
proof instead of a key: a token or signature (`/s/<token>`, `/api/hooks`,
|
|
17
17
|
`/api/inbox`, `/api/events`, `/api/approve`, `/api/unsubscribe`, `/api/git`, `/api/billing/stripe`,
|
|
18
18
|
`/api/billing/confirm`), the sign-in and recovery steps (`/api/auth/signup`,
|
|
19
|
-
`login`, `forgot`, `reset`, `verify`, `mfa/verify`, `/api/cli/login`,
|
|
20
|
-
`/api/cli/authorize` GET, `/api/oauth/callback`), `/api/healthz`, `/api/status`, `/api/status/history`, `/api/version` and `/api/openapi.json`.
|
|
19
|
+
`login`, `forgot`, `reset`, `verify`, `mfa/verify`, `session`, `/api/cli/login`,
|
|
20
|
+
`/api/cli/login/<id>`, `/api/cli/authorize` GET, `/api/oauth/callback`), `/api/healthz`, `/api/status`, `/api/status/history`, `/api/version` and `/api/openapi.json`.
|
|
21
21
|
|
|
22
22
|
```
|
|
23
23
|
Authorization: Bearer <api-key> # machines. Settings → API keys
|
|
@@ -65,7 +65,7 @@ api workspaces/$WS/flows/publish/run -X POST # start a flow
|
|
|
65
65
|
|
|
66
66
|
| Route | Methods | |
|
|
67
67
|
|---|---|---|
|
|
68
|
-
| `/api/workspaces` | GET, POST | list; create (`name`, `template
|
|
68
|
+
| `/api/workspaces` | GET, POST | list; create (`name`, plus one of: `template: true` — blank, just an AGENTS.md, what the dashboard sends; `starter: true` — the example researcher, writer and publish flow; `demo: true`; `files`) |
|
|
69
69
|
| `/api/workspaces/<ws>` | GET, PATCH, DELETE | read; rename or re-describe (`name`, `description`), or set its defaults (`timezone`, `budget`, `notify` — null clears one, edited in AGENTS.md in place); delete |
|
|
70
70
|
| `/api/workspaces/<ws>/vocabulary` | GET | which document kinds this workspace admits — its `docs` list is what `[[ ]]` resolves: knowledge, memory, `state/` and `storage/` files, plus the folders themselves |
|
|
71
71
|
|
|
@@ -113,12 +113,18 @@ swap happen in one step, so nothing can slip in between. Absent, no check.
|
|
|
113
113
|
Never touched by a deploy: `runs/`, `state/`, `secrets.json`, and any memory an
|
|
114
114
|
agent wrote that the push does not mention.
|
|
115
115
|
|
|
116
|
+
| Route | Methods | |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `/api/workspaces/<ws>/deploy` | POST | deploy the whole workspace from a list of files — checked as a whole first, refused with `422` and `issues` when it does not check out. `dryRun`, `force`, `expectRemoved` below. Idempotent |
|
|
119
|
+
| `/api/workspaces/<ws>/runtimes` | GET | the `runtime:` builds a deploy started, each with its `state` — below |
|
|
120
|
+
|
|
116
121
|
`GET /api/workspaces/<ws>/runtimes` answers what a deploy started on the
|
|
117
122
|
platform: every distinct `runtime:` the workspace's agents need
|
|
118
|
-
(`fingerprint`, `agents`, `python`, `packages`, `npm`,
|
|
119
|
-
|
|
123
|
+
(`fingerprint`, `agents`, `python`, `packages`, `npm`, `system` — the Debian
|
|
124
|
+
packages it apt-installs — and `rejected` for entries that are not
|
|
125
|
+
requirements), each with the `state` of the build the
|
|
120
126
|
platform ran for it — `pending`, `building`, `ready`, or `failed` with the
|
|
121
|
-
installer's
|
|
127
|
+
installer's `error`, its cause on the first line. `foldrun deploy` polls it after a deploy.
|
|
122
128
|
|
|
123
129
|
## Flows
|
|
124
130
|
|
|
@@ -144,10 +150,16 @@ everywhere else a run is live.
|
|
|
144
150
|
| Route | Methods | |
|
|
145
151
|
|---|---|---|
|
|
146
152
|
| `/api/workspaces/<ws>/agents` | GET, POST | list; create (`name`) |
|
|
153
|
+
| `/api/workspaces/<ws>/agents/<agent>` | PATCH | edit one line of an agent's frontmatter in place, as the flow canvas does: `{ list: { key, name, action } }` — `key` one of `subagents`, `agents`, `tools`; `action` `add` or `remove` — or `{ set: { key: "description", value } }`. Only that key's lines change — comments, key order and prose stay. Answers `{ ok, changed }`; 404 for an unknown agent. `workspace:write` |
|
|
147
154
|
| `/api/workspaces/<ws>/agents/<agent>/run` | POST | run one agent alone (`task`; `test: true` for a test run) |
|
|
155
|
+
| `/api/workspaces/<ws>/agents/import` | GET, POST | copy an agent in from another workspace of the account. GET lists what could be imported (every other workspace you can read, with its agents). POST `{ from, agent, as? }` copies `agents/<agent>/` — its agent.md and own skills/, scripts/, knowledge/; never memory/, outputs/, state/ — and answers `{ name, files, warnings }`; `warnings` names tools, skills, scripts and secrets the copy uses that this workspace lacks. 409 on a name clash (pass `as`), 404 for an unknown workspace or agent, 403 when `from` is outside your key's scope. Idempotent |
|
|
156
|
+
| `/api/workspaces/<ws>/export` | GET | this workspace, one flow or one agent as a `.zip` — `?kind=flow&name=<flow>` (the flow, every flow it runs as a step, every agent those steps and their colleagues name, and the workspace tools, skills and scripts they grant), `?kind=agent&name=<agent>` (its folder and what it grants), or neither for the whole authored tree. Never memory/, state/, outputs/, storage, runs or a secret's value; `foldrun-package.json` inside says what it is. Any role that can read the workspace |
|
|
157
|
+
| `/api/workspaces/<ws>/import` | POST | take a package in; the body is the `.zip` (`content-type: application/zip`, at most 20 MB). `?dryRun=1` answers `{ kind, name, from, creates, added, overwritten, unchanged, needs: { secrets, tools, skills, scripts, agents, flows } }` plus `skipped` (entries it will not write: memory, state, anything outside the authored folders) and writes nothing. Without it, applies as one revision; 409 when it would replace a file with different text unless `?overwrite=1`, or when a file cannot be written there (all or nothing). Never deletes. A `Content-Length` is required (411 without). A workspace package into a name that does not exist makes that workspace (`workspace:manage`, the plan's workspace count). 400 for anything not a package, or one holding memory/, state/ or outputs/. Idempotent |
|
|
148
158
|
| `/api/workspaces/<ws>/evals` | GET, POST | list; create (`name`) |
|
|
149
159
|
| `/api/workspaces/<ws>/evals/<eval>/run` | POST | run one eval |
|
|
150
160
|
| `/api/workspaces/<ws>/tools/<tool>/test` | POST | exercise one tool (`args`, `path`). An http tool with an `operations:` allowlist is probed only with a GET to one of its operations (no `path`: the first GET without parameters), and fails if the allowlist resolves nothing, as a run does |
|
|
161
|
+
| `/api/workspaces/<ws>/hooks` | GET | the [hooks](hooks) this workspace can turn on — its own and the library's (shadowing marked), invalid ones with the reason, and who turns each on — plus `recent`: every deny, rewrite and failure hooks wrote in the last `?runs=` runs (1–500, default 50), newest first; `?all=1` includes notes and context |
|
|
162
|
+
| `/api/workspaces/<ws>/hooks/<name>/test` | POST | run one [hook](hooks) on a sample event: body `{ event }`, answer the hook's own (`ok`, `decision`, `reason`, `input`, `context`, `reply`, `note`, `failure`, `ms`). Shares the `hooks/` path with flow webhooks below — `/test` is only ever a hook's. Answers `404` for no such hook, `422` for an invalid `hook.md`, `400` for an event that is not an object. On an isolated install hook code never runs in the platform's own process, so this answers with where to test it instead (`foldrun hook test` locally, or a test run) |
|
|
151
163
|
|
|
152
164
|
Running an agent alone is the fastest way to test a prompt change without
|
|
153
165
|
paying for the steps around it.
|
|
@@ -162,22 +174,22 @@ paying for the steps around it.
|
|
|
162
174
|
| `/api/workspaces/<ws>/runs/<id>/stop` | POST | destroys the sandbox, not just the record |
|
|
163
175
|
| `/api/workspaces/<ws>/runs/<id>/rerun` | POST | again — `step` to start partway, or `agent` to start at the first step that agent runs (one of the two is required). Takes `?wait=true&timeout=` like a flow start |
|
|
164
176
|
| `/api/workspaces/<ws>/runs/bulk` | POST | one action on many runs at once. `action` is `stop` or `rerun`; the runs are named by any of `runIds`, `status`, `flow`, `since`, `until`, AND-ed together, and a call naming none of them is refused rather than treated as "everything". `from` picks the step a rerun starts at, `dryRun: true` answers with what would be touched and touches nothing. At most 100 runs per call — past that it answers `422` with the count, because a filter matching more than that is almost certainly the wrong filter. Each run is its own outcome: the reply carries `matched`, `succeeded`, `failed` and a `runs` array, so one run that finished a second ago does not abort the other thirty |
|
|
165
|
-
| `/api/workspaces/<ws>/runs/<id>/approve` | POST | release a run parked on a human |
|
|
177
|
+
| `/api/workspaces/<ws>/runs/<id>/approve` | POST | release a run parked on a human: `decision` (`approve` or `reject`), `note`, `reason`, `step` (one of several waiting). `payload` (a string, or JSON sent as its text, up to 20000 characters) releases a step parked on `wait: event` with that payload, as its event URL would, recorded as released by the caller — 400 with `reject` or when no such step is waiting. `run:start`. Idempotent |
|
|
166
178
|
| `/api/workspaces/<ws>/runs/<id>/answer` | POST | answer a question an agent asked mid-step (`tools: [ask]`): `{question: <id>, answer}`. The agent is waiting in a live sandbox and gets it within a second or two. First answer wins — a second is `409` with the answer that stands; also `409` when the agent stopped waiting (its ask timed out) or its step is no longer running. `404` when the question is not this run's or has expired |
|
|
167
179
|
| `/api/workspaces/<ws>/runs/<id>/message` | POST | say something to a running agent: `{text, step?}` (at most 4 000 characters). It reaches the model after its next tool call, as context — it never changes the step's tools. `step` is the step's index in the run; left out, the only running step. With several running (a parallel group) and no `step`, `400` listing them in `running`, so a sibling never takes a message meant for another. `409` unless the run, and that step, are running. Replies `{ok, step, message}` |
|
|
168
180
|
| `/api/workspaces/<ws>/runs/<id>/archive` | GET | one file the run archived from an agent's `outputs/` — `agent` and `path` name it; the run page's Out column links here, and plays a recording or shows a picture inline. Served as bytes, never as a page; a `Range` header gets `206` with that slice, which is how a video seeks |
|
|
169
181
|
| `/api/workspaces/<ws>/runs/<id>/live` | GET | the live view: with no query, `{ agents: { <agent>: <last frame at> } }`; with `agent=`, the newest frame that agent's browser streamed as `image/jpeg` (or `204` when there is none), and `x-live-url`, `x-live-at`, `x-live-ended`, `x-live-engine` headers. Frames arrive from `web action=browse` through the step's egress lease and expire five minutes after the last |
|
|
170
182
|
| `/api/workspaces/<ws>/runs/<id>/promote` | POST | save a completed run as a regression case in `evals/<target>-regressions.md` — `evalName` to pick the file, `caseName` to name the case, `expect` (assertion lines, e.g. `["contains: $34"]`) to replace the default judge. Returns `{ ok, file, created, caseName }` |
|
|
171
|
-
| `/api/workspaces/<ws>/triggers?
|
|
183
|
+
| `/api/workspaces/<ws>/triggers?days=` | GET | why nothing happened: one row per flow saying how often its trigger fired, how often that became a run, and what the difference was — a duplicate delivery, a throttled burst, a debounced burst still being waited out, a quarantined flow, an `overlap: skip`, a fire missed while the platform was down. Plus every decision newest-first, and the hook/inbox delivery log, which answers the separate question of whether a request arrived and was authentic. `days` 1–365, default 7 (`since=`, its older name, still works — deprecated) |
|
|
172
184
|
| `/api/workspaces/<ws>/notify/deliveries` | GET | the outbound webhook log: every `notify:` url delivery from this workspace, newest first — `{ deliveries, total, limit, offset, endpoint }`. A delivery is `{ id, event, status, destination, attempts, attemptLog, lastStatusCode, lastError, nextAttemptAt, createdAt, updatedAt, body }`: `id` is the `X-Foldrun-Delivery` header; `status` is `pending` (a retry is due at `nextAttemptAt`), `delivered`, `failed` (gave up after six attempts) or `skipped` (the endpoint was switched off); each attempt is `{ n, at, statusCode, durationMs, response (first 500 characters), error, manual? }`. `body` is what was sent, except that a gate's `approveUrl`/`rejectUrl` (top level and in `links`) read `[redacted]` — they decide without a login, and this log is readable by every role; the receiver still gets them, on a redelivery too. `?status=`, `?event=`, `?limit=` (50, at most 200), `?offset=`. `endpoint` is `{ failingSince, lastSuccessAt, lastFailureAt, disabledAt, disabledReason }` or null. Kept 30 days |
|
|
173
185
|
| `/api/workspaces/<ws>/notify/deliveries/<id>/redeliver` | POST | send one delivery again now — the same id and body, signed afresh, to the URL `notify:` names today. `{ ok, attempt, delivery }`, always `200` when the delivery exists (whether the receiver took it is the answer); `404` when it does not. An accepted redelivery switches a disabled endpoint back on. Needs the permission that runs |
|
|
174
186
|
| `/api/workspaces/<ws>/notify/test` | POST | sends one real notification to whatever this workspace's `notify:` resolves to, and answers `{ ok, destination, detail }` — including the provider's own words when it failed, which are what name the problem. Always `200`: the request was handled, and the result is the answer. Needs the permission that runs, because it sends real mail |
|
|
175
|
-
| `/api/workspaces/<ws>/observe?
|
|
187
|
+
| `/api/workspaces/<ws>/observe?days=` | GET | the observability report the Observe page renders — per-agent failures and retries, per-tool calls/errors/latency, per-flow duration, day series, recent failures. `days` 1–365, default 30 (`since=`, its older name, still works — deprecated) |
|
|
176
188
|
| `/api/workspaces/<ws>/history?path=&id=&limit=` | GET | every change to the workspace, newest first — `path` narrows to one file, `id` returns one revision with its before/after and a line diff, `limit` 1–500, default 100. A deploy's revision id is its commit |
|
|
177
189
|
| `/git/<tenant>/<ws>.git` | git | the workspace as a git remote — `git clone`, `git push` (a push to main deploys and runs the evals; a push to any other branch deploys a **preview** workspace named `<ws>-preview-<branch>` from that branch's tree and runs its evals there — the scheduler never fires a preview's flows, a preview reads its source's secrets, and deleting the branch deletes the preview). A push while runs are in flight is accepted but **not** applied yet: git warns at push time, and the scheduler applies it when those runs finish. HTTP Basic: any username, an API key as the password |
|
|
178
190
|
| `/git/<tenant>/_library.git` | git | the account library as a git remote — a push replaces the shelf |
|
|
179
191
|
| `/api/workspaces/<ws>/repo?branch=` | GET, POST | GET: branches, tags, mirror settings; `?branch=` a branch's net change against main with diffs. POST `{action}`: `deploy` a `ref` (a rollback is a new commit on main), `merge` a `branch` into main and deploy, `delete-branch`, `mirror` (set `url`, https, credentials via the MIRROR_TOKEN secret), `mirror-now` |
|
|
180
|
-
| `/api/workspaces/<ws>/restore` | POST | put the workspace's source back as it was at a point in its history. `to`: a commit or its prefix, a branch or tag, an ISO date/time (the last commit on main at or before it) or an age (`90m`, `12h`, `3d`, `2w`). `{ to, dryRun: true }` answers `{ to: { sha, at, by, message }, from, added, updated, removed, files: [{ path, change, diff }], blockedBy, noop }` and writes nothing. The restore needs `confirm` set to the workspace's name (400 without) and takes the dry run's `expectRemoved`; it deploys that commit's tree as a new commit on main (undo: restore to `from`), keeps runs, `state/`, storage, secrets and agent-written memory, and is refused 409 while runs are in flight or when it would remove something not in `expectRemoved`. `workspace:write`; audited as `workspace.restored` |
|
|
192
|
+
| `/api/workspaces/<ws>/restore` | POST | put the workspace's source back as it was at a point in its history. `to`: a commit or its prefix, a branch or tag, an ISO date/time (the last commit on main at or before it) or an age (`90m`, `12h`, `3d`, `2w`). `{ to, dryRun: true }` answers `{ to: { sha, at, by, message }, from, added, updated, removed, files: [{ path, change, diff }], blockedBy, noop, issues, warnings }` and writes nothing; `issues` are the deploy checks that would refuse that tree, `warnings` the ones it would deploy with. The restore needs `confirm` set to the workspace's name (400 without) and takes the dry run's `expectRemoved`; it deploys that commit's tree as a new commit on main (undo: restore to `from`), keeps runs, `state/`, storage, secrets and agent-written memory, and is refused 409 while runs are in flight or when it would remove something not in `expectRemoved`. `workspace:write`; audited as `workspace.restored` |
|
|
181
193
|
|
|
182
194
|
`status` is one of `queued`, `running`, `awaiting-approval`, `completed`,
|
|
183
195
|
`failed`.
|
|
@@ -279,7 +291,9 @@ its header and requires a URL it can GET for itself.
|
|
|
279
291
|
|
|
280
292
|
## Secrets
|
|
281
293
|
|
|
282
|
-
|
|
294
|
+
| Route | Methods | |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| `/api/secrets?workspace=` | GET, PUT, DELETE | the vault: list which secrets exist (never a value); set one (`name` plus one shape, below); delete one (`name`). `workspace` names a workspace's own store; unsaid is the account's, which a key scoped to some workspaces may not touch. PUT is idempotent |
|
|
283
297
|
|
|
284
298
|
`PUT` takes `name` plus exactly one shape: `value` (a string), `file` (a PEM,
|
|
285
299
|
a cert), `oauth2`, `service_account`, `ssh`, or `api`. They go to the vault;
|
|
@@ -296,15 +310,17 @@ Reads return which secrets exist, never their values.
|
|
|
296
310
|
| `/api/team` | GET, POST | members with their roles and workspace scope; mint an invite — `role` (viewer \| editor \| admin, default editor), `workspaces` (names, or omit for all), `email` (bound into the invite: only that address can accept it; sent through the account's `RESEND_API_KEY` unless `send: false`). Returns `path` (the invite's relative URL), `expires`, `sent` and any `sendError` either way; the full link is in the mail |
|
|
297
311
|
| `/api/team/members/<id>` | PATCH, DELETE | change a member's role (`role`) and/or which workspaces they may open (`workspaces`: names, or null for all); remove them |
|
|
298
312
|
| `/api/team/transfer` | POST | hand the account to a member (`to`) — owner only; you become an admin |
|
|
299
|
-
| `/api/usage/history?days=` | GET | charged spend by flow, by agent and by workspace over time, read from the ledger rather than the run records — so it survives runs being pruned and reports what was billed rather than what was consumed. Buckets by week over three weeks, by day under. Each series carries its periods, its total, and the change between the last two COMPLETE periods, so a partial week never reads as a collapse. `unattributedUsd` is charges from before a line recorded its flow |
|
|
313
|
+
| `/api/usage/history?days=` | GET | charged spend by flow, by agent and by workspace over time, read from the ledger rather than the run records — so it survives runs being pruned and reports what was billed rather than what was consumed. Buckets by week over three weeks, by day under. Each series carries its periods, its total, and the change between the last two COMPLETE periods, so a partial week never reads as a collapse. `unattributedUsd` is charges from before a line recorded its flow. A caller scoped to some workspaces sees only the charges in those; `days` 1–365, default 56 |
|
|
314
|
+
| `/api/account/model` | GET, PUT, DELETE | the account's own model key — what every step runs on unless an agent, workspace or the account's AGENTS.md names a `provider:`. GET: `{ set, provider, baseUrl, format, keyHint, runsOn, platformKeyAllowed }` — never the key, only its last four characters; `runsOn` is `account` (its own key), `platform` (the platform's credential, for the operator's accounts) or `none` (runs are refused until a key is set). PUT `{ provider, apiKey, baseUrl?, format? }` (needs `secrets:write`): `provider` is a preset name (anthropic, openrouter, openai, groq, … — see providers) or `custom` with an https `baseUrl` and a `format` (anthropic or openai); a Claude login token (sk-ant-oat…) is refused for any account not allowed the platform's credential. Stored as the account secrets `FOLDRUN_MODEL_PROVIDER`, `FOLDRUN_MODEL_API_KEY`, `FOLDRUN_MODEL_BASE_URL`, `FOLDRUN_MODEL_FORMAT`. DELETE removes them. Honours Idempotency-Key on PUT; audited as model.key_set / model.key_removed |
|
|
300
315
|
| `/api/account/providers` | GET, POST | GET: the model providers this account's files actually use — each one's preset name, where it is declared, the model a check would send — plus the last daily health check. POST forces one now, which spends one minimal request at each distinct endpoint, and needs the permission that runs |
|
|
301
|
-
| `/api/usage` | GET | what has been spent, by meter |
|
|
316
|
+
| `/api/usage` | GET | what has been spent, by meter. A key or member scoped to some workspaces sees those workspaces, and totals over them; the ledger figures are the account's, as the wallet's are |
|
|
302
317
|
| `/api/approvals` | GET | every gate waiting on a person, across workspaces — one entry per waiting **step** (`workspace`, `runId`, `flow`, `step`, `agent`, `question`, `asked`, `waitingSince`), oldest first. By step status, not run status: a decided gate leaves the list at once, before the worker resumes the run. `wait: event` steps are not listed; no button answers them. Beside it, `questions`: every question an agent is asking mid-step in a running run (`kind: "question"`, `id`, `workspace`, `runId`, `flow`, `agent`, `question`, `options?`, `askedAt`) — answered with `…/runs/<id>/answer` |
|
|
303
318
|
| `/api/account/export` | GET | the owner's copy of what the platform holds about the account, as one JSON file (`foldrun-<account>-export.json`): `workspaces` (with their flows), `members` (email, role, scope, since), `runs` (an index — id, workspace, flow, status, times, headline) and the whole `ledger` (each line's charge; the platform's provider cost is left out). `billing:manage`, so the owner; audited as `account.exported`. The markdown itself comes from `foldrun pull` or the source API |
|
|
304
319
|
| `/api/account/close` | POST | the owner asks to close the account — in person, never a key, with `confirm` set to the account's name and an optional `reason`. Not a delete: the account is suspended at once (runs in flight finish, sign-in and the export still work), `closeRequested` is set on its admin state, and the super admin is mailed to settle the last invoice and any refund. Returns `{ ok, suspended, notified }`; 409 when already asked. Hosted installs only |
|
|
305
320
|
| `/api/account/backups` | GET | what is backed up for this account: `policy` (`schedule`, `keepLocal`, `keepOffsiteDays`, `encrypted`, `covers`, `excludes`), `last`, `backups` (each nightly archive that holds this account, newest first: `id`, `at`, `bytes` (the encrypted object), `sha256` (the archive inside the encryption — what a decrypt must reproduce), `kinds` — files, database, keys — `verified`, `offsite`, `keepDays`, `encrypted` (sealed with age before it left the server; false for archives taken before October 2026), `recipient` (the key's fingerprint, `sha256:<16 hex>`, or null)), `requests` (earlier restore requests) and `workspaces` (`name`, `head`, `history`, `lastChange`) — what a restore can reach. See [Backups and restore](backups-and-restore.md) |
|
|
306
321
|
| `/api/account/backups/requests` | GET, POST | ask the platform team to restore what an account cannot restore itself. POST `{ what, target, workspace?, backupId?, note? }` (`what` is runs, state, storage or everything) records the request, mails the operator and audits it as `backup.restore_requested`; answers 201 `{ ok, request }` with `notified` saying whether the mail went. Restores nothing. `workspace:manage`, and a member scoped to some workspaces must name one. GET lists this account's requests |
|
|
307
|
-
| `/api/account/api` | GET, PATCH | how this account meets the API — GET `{ version: { current, pinned, effective, supported, supportMonths }, limits: { plan, windowSeconds, perCredential: { read, write }, perAccount: { read, write } } }
|
|
322
|
+
| `/api/account/api` | GET, PATCH | how this account meets the API — GET `{ version: { current, pinned, effective, supported, supportMonths }, limits: { plan, windowSeconds, perCredential: { read, write }, perAccount: { read, write } }, usage }`. `usage` is the last 30 days — `{ days, versions: [{ version, requests, lastSeen }], deprecated: [{ route, requests, lastSeen, deprecatedAt, sunsetAt, link, callers: [{ credential, requests, lastSeen }] }] }`, `credential` naming a key by its label and prefix — or `null` when it could not be read; PATCH `{ version: "YYYY-MM-DD" \| null }` pins the version requests without a `Foldrun-Version` header get (null: always current). `keys:manage`. See [api-usage](api-usage.md) |
|
|
323
|
+
| `/api/account/flags` | GET | the feature flags as they stand for this account — `{ flags: [{ name, description, owner, default, on, source, rollout }] }`, `source` being what decided it: `account` (an override for this account), `global` (the install-wide setting), `rollout` (a percentage of accounts, by a stable hash of account and flag; `rollout` is the percentage) or `default`. Read-only, every role; only the super admin changes a flag. See [Feature flags](feature-flags.md) |
|
|
308
324
|
| `/api/openapi.json` | GET | this API as an OpenAPI 3.1 document — every route and method, bearer auth, the rate-limit, version and Idempotency-Key headers, and request/response schemas for runs, flow runs, deploy, source, secrets, keys and billing. No key needed. Generated from the route files by `scripts/gen-openapi.mjs`; the dashboard renders it at Docs → API reference, `foldrun api spec` saves it |
|
|
309
325
|
| `/api/account` | GET, PATCH | the account's defaults from its AGENTS.md frontmatter — `timezone`, `budget` (a USD cap — a number for a month, `"60/day"`, `"60/week"`, or `{ usd, period }`; null is no limit), `notify` (`{ url?, email?, events }`); PATCH sets the keys sent, null clears one, and edits the file in place |
|
|
310
326
|
| `/api/audit?since&until&action&actor&workspace&limit&cursor&format` | GET | the account's audit log, newest first — the caller's account only, whatever a header, query or body names. The owner and unscoped admins (people, or `admin` keys); `audit:read`, and refused to a member or key scoped to some workspaces, because the log is the whole account's. `since`/`until`: `7d`, `24h`, `30m` or a date; `action`: exact (`apikey.created`) or a family (`apikey`); `actor`: exact (an email, `key:<label>`, `support:<email>`); `workspace`: entries about one; `limit` 1–1000 (100; 1000 for CSV); `cursor`: the `next` of the page before. JSON `{ entries: [{ id, at, actor, action, subject, detail }], next, retention }`; `format=csv` downloads the same rows (`x-next-cursor` header when there are more). Records sign-ins and refusals, sign-outs, session revokes, password and email changes, MFA on/off, key mint/rotate/revoke, invites, joins, role/scope changes, removals, ownership transfers, secrets set/deleted (by name — never a value), deploys, workspace deletes, plan and billing changes, export and close requests, and `support.viewed`/`support.ended`. The super admin's own levers (`operator.*`) are the platform's record and not listed. Kept for the life of the account |
|
|
@@ -347,7 +363,7 @@ person: a key names an account, never a who.
|
|
|
347
363
|
| `/api/billing/card` | POST | save a card |
|
|
348
364
|
| `/api/billing/details` | GET, PUT | who the invoices are made out to — GET `{ details, stripe, customer, tax }` for anyone who can see billing; PUT (the owner) `legalName` (required), `email`, `abn` (checked against the ABN rule), `address` `{ line1, line2, city, state, postcode, country }` (two-letter country) or `null`. Every problem comes back at once as `errors` with a `400`. Kept first, then sent to the Stripe customer (name, address, the ABN as its `au_abn` tax id): answers `synced`, and `syncError` with Stripe's words when Stripe refused — the details stand either way |
|
|
349
365
|
| `/api/billing/portal` | POST | a Stripe Billing Portal session for the account's customer (the owner): answers `url`. Optional `returnTo`, a `/dashboard/…` path to come back to. `501` without Stripe |
|
|
350
|
-
| `/api/billing/statement?month=&format=` | GET | one month of the ledger as a statement. JSON by default; `format=csv` downloads it with the month, the account and the spent and added totals in its header. `month` is `YYYY-MM`, default the current month. What the customer sees is the charge; the platform's provider cost is not on it |
|
|
366
|
+
| `/api/billing/statement?month=&format=` | GET | one month of the ledger as a statement. JSON by default; `format=csv` downloads it with the month, the account and the spent and added totals in its header. `month` is `YYYY-MM`, default the current month. What the customer sees is the charge; the platform's provider cost is not on it. The whole account's money, so refused (403) to a member or key scoped to some workspaces, as the audit log is |
|
|
351
367
|
| `/api/billing/wallet` | GET, PUT | the balance summary (burn, runway, daily spend, month to date, the suggested top-up), the refill rule and the low-balance email — GET `{ enabled, stripe, summary, cardSaved, autoTopUp, email, dunning }`, PUT `autoTopUp`, `email`. `dunning.status` is `none`, `grace` (a plan invoice failed; runs go on until `graceUntil`) or `suspended` (new runs paused until it is paid), with `payUrl`, `amountDue` (minor units) and `currency` |
|
|
352
368
|
| `/api/billing/plans` | GET | the catalogue (four plans: fee, credits, per-credit price, runs at once, workspaces, lane), where this account stands (plan, status, this cycle's credits and what is left, when it resets), the balance in dollars and credits, and any overdraft the platform has allowed |
|
|
353
369
|
| `/api/billing/subscribe` | POST, DELETE | POST `plan` — starter, creator, pro or scale. With no subscription yet, answers `url`: a Stripe Checkout in subscription mode; the return trip starts the first cycle. With a live one, changes it at Stripe at once, prorated (an upgrade is invoiced today and the bigger bundle lands with it; a downgrade comes back as credit on the next invoice), and answers `changed: true`. POST `resume: true` takes a pending cancellation off — nothing is charged and no cycle moves. DELETE ends the plan with the current cycle: the credits stand until then, and credits bought on top are kept |
|
|
@@ -360,7 +376,7 @@ What a platform runs, and what changed. See [Versions and releases](versions).
|
|
|
360
376
|
| Route | Methods | |
|
|
361
377
|
|---|---|---|
|
|
362
378
|
| `/api/version` | GET | open, no key. `{ version, released_at, build, api, components: { core, platform, web, docs, infra, cli }, packages: { core, cli }, browsers, browser_versions }` — `version` is `vYYYY.MM.DD.N` (`dev` on an install the deploy did not build); `components` are short commit shas (`cli` is the CLI commit released alongside); `packages.cli` is that CLI's npm version; `api` is the API contract's date version; `browsers` is the engines the runner image was built with (its `/opt/browser/engines.json`, read by the deploy — see [Engines](tools#engines)), `null` when the deploy did not say; `browser_versions` is their versions where the manifest has a dotted one (`{ chromium: "153.0.8010.12", chrome: "154.0.8037.58", lightpanda: "0.4.1" }`), which `foldrun check` compares with a pinned `user_agent`, `null` when none. `Cache-Control: no-store` |
|
|
363
|
-
| `/api/changelog?limit=` | GET | `{ version, releases: [{ version, released_at, previous, components, notes }] }
|
|
379
|
+
| `/api/changelog?limit=` | GET | `{ version, api, releases: [{ version, released_at, previous, components, notes }] }`. `api` is the API's own changelog: `{ current, supportMonths, versions: [{ version, changes }], deprecations: [{ route, deprecatedAt, sunsetAt, link }] }` — every version a caller may pin, newest first, with the breaking changes it introduced, and the routes on their way out. `releases` newest first — 10 by default, at most 50. `notes` is `{ features, fixes, docs, other }`, each a list of `{ repo, sha, subject }` grouped from commit subjects, or `null` when the deploy could not read what changed. Needs a key or a session |
|
|
364
380
|
|
|
365
381
|
Every `/api` response carries the version in its headers, refusals included,
|
|
366
382
|
so a client learns what it is talking to without a second request:
|
|
@@ -431,5 +447,6 @@ signed-in person, never by a key — mints an ordinary API key at their role.
|
|
|
431
447
|
| `/api/inbox/<tenant>/<ws>/<flow>` | POST | the token as for a hook (`Authorization: Bearer`, `X-Foldrun-Token`, or `?token=`). An inbound email for a `trigger: email` flow — JSON (`from`, `to`, `subject`, `text` or `html`; Resend's `data` wrapper understood) or an inbound-parse form (Mailgun, SendGrid). Same token as the hook |
|
|
432
448
|
| `/api/events/<tenant>/<ws>/<id>?token=` | POST | release a run parked on `wait: event`; the body becomes the step's event payload. The token is derived from the run id, and the URL is printed in the waiting step's trace. `409` when the run is not waiting on an event |
|
|
433
449
|
| `/api/approve/<tenant>/<ws>/<id>?token=` | GET, POST | decide a run parked on a human, from the link in its notification. GET shows what is waiting and two buttons — it never decides, because inbox link-checkers follow GETs. POST `decision` (`approve` or `reject`) and an optional `note`, form-encoded or JSON; answers JSON when `Accept` prefers it. The token is derived from the run id under the install key, not stored, so a key rotation kills every link. Needs `FOLDRUN_PUBLIC_URL` to be minted at all |
|
|
450
|
+
| `/api/workspaces/<ws>/hooks/<flow>` | GET | read the flow's webhook URL without changing it: `{ ok, flow, path, url }` — `path` carries the token, `url` is it made absolute with `FOLDRUN_PUBLIC_URL` (null when that is unset). `workspace:manage`, as rotate and the settings page that shows it, since the URL is a credential. `404` for no such flow, `409` for a flow that is not `trigger: webhook` |
|
|
434
451
|
| `/api/workspaces/<ws>/hooks/<flow>/rotate` | POST | new token, old URLs stop working |
|
|
435
452
|
| `/api/git/<tenant>/<ws>?branch=&dir=` | POST | GitHub push webhook — HMAC-signed, fetches the tarball at that commit and deploys it |
|
|
@@ -6,75 +6,47 @@ different things:
|
|
|
6
6
|
| | What it brings back | Who does it | How far back |
|
|
7
7
|
|---|---|---|---|
|
|
8
8
|
| **A workspace's history** | its agents, flows, knowledge, skills, tools, scripts — the markdown | you, from Settings → Backups, `foldrun restore` or the API | every change it ever had |
|
|
9
|
-
| **
|
|
9
|
+
| **A database dump before a migration** | the database: people, API keys, the ledger and every balance, the queue, the audit log | the platform team, when you ask | the last 10 deploys that changed the schema |
|
|
10
10
|
|
|
11
11
|
You do not need to switch either on.
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
**nightly
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
the key file — is encrypted with [age](https://age-encryption.org) to a
|
|
29
|
-
public key whose private half is **not** on the server: the server can make
|
|
30
|
-
a backup and cannot open one, and neither can anyone holding the object
|
|
31
|
-
storage it is copied to. The archive is encrypted as it is written, so it is
|
|
32
|
-
never stored unencrypted, even on the server. The job refuses to run rather
|
|
33
|
-
than copy anything unencrypted. Snapshots taken before this change
|
|
34
|
-
(October 2026) were copied off the server unencrypted, all but the key file:
|
|
35
|
-
the first encrypted run deletes those copies from object storage, and the
|
|
36
|
-
server's own copies of those nights age out of the 14 kept.
|
|
37
|
-
|
|
38
|
-
Before an archive is kept it is checked: the database dump must be readable
|
|
39
|
-
by `pg_restore --list` and list table data, row counts are taken from the
|
|
40
|
-
live database at the same moment, and the archive is listed in full and
|
|
41
|
-
its checksum recorded — of the very bytes being encrypted, in the same pass.
|
|
42
|
-
An archive that fails the check is deleted and the older ones are kept.
|
|
43
|
-
That an encrypted archive decrypts, with the private key, to exactly that
|
|
44
|
-
checksum is proved by an automated test of the whole job on every change to
|
|
45
|
-
it; the server cannot prove it nightly, because it does not have the key.
|
|
46
|
-
|
|
47
|
-
**Kept:** the last 14 on the server, and 15 days in object storage off it
|
|
48
|
-
(Cloudflare R2).
|
|
49
|
-
|
|
50
|
-
A full restore from one of these archives was performed on 2026-09-05, from
|
|
51
|
-
the R2 copy: the archive matched the server's byte for byte and restored the
|
|
52
|
-
workspaces, run records, the database and the sealed key file. That was
|
|
53
|
-
before the archive itself was encrypted; *not yet performed* on an encrypted
|
|
54
|
-
one — the next restore drill is.
|
|
13
|
+
## There is no nightly snapshot
|
|
14
|
+
|
|
15
|
+
The hosted platform today is a development server, and since 11 October 2026
|
|
16
|
+
it takes **no nightly backup**. Run records, `state/`, storage's index and
|
|
17
|
+
the vault are on its data volume and are not copied anywhere on a schedule.
|
|
18
|
+
The last full archive is from 7 October 2026 (`foldrun-data-20261007-0330`,
|
|
19
|
+
in Cloudflare R2), encrypted with [age](https://age-encryption.org) to a key
|
|
20
|
+
the server does not hold.
|
|
21
|
+
|
|
22
|
+
What remains is the **database dump taken before every deploy that runs a
|
|
23
|
+
new migration**. It is checked with `pg_restore --list` before it is kept,
|
|
24
|
+
encrypted to the same off-server key (the server can write it and cannot
|
|
25
|
+
read it), and the deploy refuses to go ahead without it, because a migration
|
|
26
|
+
has no way back. The newest 10 are kept on the server and a copy goes to R2.
|
|
27
|
+
It holds the database only, not the data volume.
|
|
55
28
|
|
|
56
29
|
### What it does not hold
|
|
57
30
|
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
31
|
+
- Workspace files, run records, `state/`, `memory/`, the vault and each
|
|
32
|
+
workspace's git history: the data volume is not backed up. A workspace's
|
|
33
|
+
source can still be restored from its own history (below).
|
|
34
|
+
- **Storage file bytes.** These live in object storage (R2), not on the data
|
|
35
|
+
volume. *Unverified:* whether a storage file deleted by mistake can still
|
|
36
|
+
be recovered from object storage. Ask, and say when it was deleted.
|
|
63
37
|
- Anything that only existed inside a sandbox while a step ran.
|
|
64
38
|
- What model providers and connected services hold on their side.
|
|
65
39
|
|
|
66
40
|
A self-hosted install backs up however its operator set it up; this page
|
|
67
|
-
describes the hosted platform. `foldrun-infra/dev/backup.sh`
|
|
68
|
-
you want the same for yours: it needs `age` and
|
|
69
|
-
(your age public key) and will not run without them.
|
|
41
|
+
describes the hosted platform. `foldrun-infra/dev/ci/premigrate-backup.sh`
|
|
42
|
+
is the pre-migration dump, if you want the same for yours: it needs `age` and
|
|
43
|
+
`FOLDRUN_BACKUP_RECIPIENT` (your age public key) and will not run without them.
|
|
70
44
|
|
|
71
45
|
## Seeing your snapshots
|
|
72
46
|
|
|
73
|
-
Settings → **Backups**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
each one marked encrypted or not (the ones from before October 2026 are
|
|
77
|
-
not).
|
|
47
|
+
Settings → **Backups** and `foldrun backups` list the archives recorded for
|
|
48
|
+
your account. With the nightly job gone, nothing new is added to that list:
|
|
49
|
+
it ends at the last nightly run, on 7 October 2026.
|
|
78
50
|
|
|
79
51
|
```sh
|
|
80
52
|
foldrun backups # the same, in the terminal
|
|
@@ -84,14 +56,10 @@ foldrun backups # the same, in the terminal
|
|
|
84
56
|
GET /api/account/backups
|
|
85
57
|
```
|
|
86
58
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`sha256:` and 16 hex digits of the public key) — a change of fingerprint is
|
|
92
|
-
a change of key. *Not yet verified on the server:*
|
|
93
|
-
the recording landed on 2026-10-01, so the list starts with the first
|
|
94
|
-
nightly run after that deploy — older archives exist but are not listed.
|
|
59
|
+
An account sees only the archives it is in, and its own name as the only
|
|
60
|
+
entry in `accounts`. Each record says `encrypted` and names the key it was
|
|
61
|
+
encrypted to by fingerprint (`recipient`, `sha256:` and 16 hex digits of the
|
|
62
|
+
public key).
|
|
95
63
|
|
|
96
64
|
## Restoring a workspace yourself
|
|
97
65
|
|
|
@@ -138,8 +106,9 @@ deploy any one of them — the same operation, by commit instead of by time.
|
|
|
138
106
|
## Asking for a restore from a snapshot
|
|
139
107
|
|
|
140
108
|
Run records, `state/`, storage and the account's records (people, keys,
|
|
141
|
-
balances) come back only from
|
|
142
|
-
|
|
109
|
+
balances) come back only from an archive: the database from a pre-migration
|
|
110
|
+
dump, the rest only from the last full archive (7 October 2026). That restore
|
|
111
|
+
replaces a volume and a database every account shares, and the key that opens the
|
|
143
112
|
sealed half of the archive is deliberately not on the server, so it is done
|
|
144
113
|
by the platform team rather than by a button.
|
|
145
114
|
|
|
@@ -98,6 +98,10 @@ per-seat fee would tax the approver the safety story depends on.
|
|
|
98
98
|
**Bring your own model key** and tokens cost no credits at all — the model
|
|
99
99
|
is billed to your own provider account — so a plan then buys sandbox time
|
|
100
100
|
and the platform, which is most of what a BYOK customer's credits go on.
|
|
101
|
+
A step counts as BYOK when it ran on a key of the account's own (Settings →
|
|
102
|
+
Model, or a `provider:` block); the run page says which credential each step
|
|
103
|
+
used. On a hosted platform every customer account brings one
|
|
104
|
+
([providers](providers.md#your-accounts-own-model-key)).
|
|
101
105
|
|
|
102
106
|
**Going into minus.** Ordinarily an empty balance starts no run. The
|
|
103
107
|
platform can allow an account to run below zero — to a floor, or with none —
|
|
@@ -166,7 +170,7 @@ avatar, with eight tabs:
|
|
|
166
170
|
|
|
167
171
|
| tab | what it is |
|
|
168
172
|
|---|---|
|
|
169
|
-
| **Balance** | the number, large, with how long it lasts in words, thirty days of spend as a sparkline, what is held against runs in flight, and Add credit with the amount that covers a month already chosen |
|
|
173
|
+
| **Balance** | the number, large, with how long it lasts in words, thirty days of spend as a sparkline, what is held against runs in flight, and Add credit with the amount that covers a month already chosen. While billing is off on an install (self-hosted, or a test platform), Add credit puts credit on by hand instead — no payment, and an empty balance stops nothing then — the same as `foldrun billing credit <usd>` |
|
|
170
174
|
| **Plan** | the plan you are on, this cycle's credits and when they reset, and the plans you could move to |
|
|
171
175
|
| **Usage** | what was consumed, from the run records, and what each flow and agent cost by week from the ledger |
|
|
172
176
|
| **Statement** | the ledger as a statement: grouped by day, human labels, month-to-date, filters, and a CSV for the month |
|