foldrun 2.1.0 → 2.3.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/bin/foldrun.mjs +23 -9
- package/docs/agents.md +54 -2
- package/docs/api.md +22 -13
- package/docs/assistant.md +32 -2
- package/docs/budgets-and-billing.md +46 -5
- package/docs/checks.md +101 -2
- package/docs/cli.md +20 -12
- package/docs/evals.md +9 -4
- package/docs/flows.md +6 -1
- package/docs/getting-started.md +5 -2
- package/docs/network.md +22 -7
- package/docs/notifications.md +54 -0
- package/docs/providers.md +8 -2
- package/docs/runs.md +199 -6
- package/docs/security.md +3 -2
- package/docs/tools.md +29 -1
- package/docs/workspaces.md +42 -0
- package/package.json +3 -3
- package/src/commands.mjs +563 -25
- package/src/guide.mjs +1 -1
package/bin/foldrun.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// foldrun approvals what is waiting for a person, anywhere in the account
|
|
18
18
|
// foldrun approve <run-id> release a waiting gate (asks first) — reject refuses it
|
|
19
19
|
// foldrun stop <run-id> kill a run in flight
|
|
20
|
-
// foldrun account the account's own defaults — timezone, notify, budget, concurrency
|
|
20
|
+
// foldrun account the account's own defaults — timezone, notify, budget, concurrency, deploy_block, network_enforce
|
|
21
21
|
// foldrun schedule every flow in the account that fires on a clock, and when it fires next
|
|
22
22
|
// foldrun triggers why nothing ran: fired vs started per flow, and each reason
|
|
23
23
|
// foldrun webhooks <verb> deliveries / redeliver <id> — the notify: webhooks a workspace sent, and each attempt
|
|
@@ -101,11 +101,12 @@ const HELP = `foldrun — agents are just folders
|
|
|
101
101
|
foldrun flow new <name> one more flow — its first step names an agent you already have
|
|
102
102
|
foldrun flow new <name> --platform make it in the deployed workspace from a shape (--pattern pipeline|review-loop|fan-out|debate|router, --to)
|
|
103
103
|
foldrun tool new <name> one more tool: a folder with its program beside it (--transport, --language; --platform in the deployed workspace, --template <gallery entry>)
|
|
104
|
-
foldrun agent ls the agents deployed in a workspace: model, description (--to, --json) — flow ls the flows and how each fires
|
|
104
|
+
foldrun agent ls the agents deployed in a workspace: model, last run, owner, description (--to, --json, --stale [days]) — flow ls the flows and how each fires
|
|
105
105
|
foldrun check [dir] validate every workspace here, and the shared library
|
|
106
106
|
foldrun check --to <ws> validate the DEPLOYED copy instead — fetched from the platform, checked here
|
|
107
107
|
foldrun check --security only the security rules: hard-coded secrets, hidden text, agents that can leak, unpinned code — fails on a warning
|
|
108
108
|
foldrun check --rules every rule check knows: id, level, what it guards against (--json)
|
|
109
|
+
foldrun check --to <ws> --online also the network checks, run by the platform: known vulnerabilities for exact runtime pins (OSV) and remote MCP servers' tool text — and what could not be checked
|
|
109
110
|
foldrun agent run <name> run one agent once on the platform (--task "…", --to, --wait, --test)
|
|
110
111
|
foldrun agent link <agent> add to its team, in its frontmatter: --subagent <worker>, --consult <agent> or --can-ask (--description for a worker with none)
|
|
111
112
|
foldrun agent unlink <agent> take one away: --subagent <name>, --consult <name> or --can-ask
|
|
@@ -136,22 +137,32 @@ const HELP = `foldrun — agents are just folders
|
|
|
136
137
|
foldrun run <target> run an agent or flow HERE (target: name, or flow:name) — on the platform: flow run / invoke a flow, agent run one agent
|
|
137
138
|
foldrun eval [name] run one eval, or all of them (--to <workspace>: the deployed evals, run on the platform)
|
|
138
139
|
foldrun eval new <name> a new eval in the deployed workspace, its agent the first one there — edit its cases, then eval <name> --to (--to)
|
|
139
|
-
foldrun promote <run-id> keep a finished run as a regression case in evals/ (--eval <name>, --expect "contains: …" repeatable, --case <name>)
|
|
140
|
+
foldrun promote <run-id> keep a finished run as a regression case in evals/ (--eval <name>, --expect "contains: …" repeatable, --case <name>); a run that went wrong takes --should "what it should have done"
|
|
140
141
|
foldrun probe <model> live check: can this model hold a tool loop here?
|
|
141
142
|
foldrun logs [run-id] recent runs in one workspace, or one run's full event trail (--local: this machine's)
|
|
142
|
-
foldrun runs what has run lately, across the account — --status, --verdict, --since, --to, --limit
|
|
143
|
+
foldrun runs what has run lately, across the account — --status, --verdict, --outcome (good, done, quiet, blocked, bad, failed, needs-you, …), --since, --to, --limit; search with --tool, --tool-error, --agent, --model, --min-tokens 50k, --min-secs, or --view <name>
|
|
144
|
+
foldrun runs views your saved run searches (--local prints exactly what logs --local prints)
|
|
145
|
+
foldrun runs doctor <run-id> the run doctor: the assistant reads a failed run and the files it names, says why (platform, check, work or outside) and proposes the fix as diffs, saved on yes (--to, --yes, --dry-run, --json)
|
|
143
146
|
foldrun runs rm <run-id> delete a run's record and archived outputs; a live one is stopped first — asks first, --yes means it
|
|
144
147
|
foldrun report <run-id> one run, whole: header, every step, what it wrote and what is still waiting
|
|
145
148
|
foldrun report <run-id> get <agent>/<path> download a file the run archived from that agent's outputs/ (--file <path>|-, --force)
|
|
149
|
+
foldrun report <run-id> --compare [<run-id>] beside the flow's last good run (or the one named): changed files and what differs step by step (--json)
|
|
150
|
+
foldrun report <run-id> --tree the run as one tree: step → model call → tool call, with what went wrong marked (--json)
|
|
151
|
+
foldrun report <run-id> --calls also every model call, and the tool calls whose input and output were recorded
|
|
152
|
+
foldrun report <run-id> call <call-id> one tool call's recorded input and output, secrets masked (--json)
|
|
146
153
|
foldrun report <run-id> live the live browser view: which agents have a frame; --agent <name> saves the newest as a JPEG (--file <path>|-)
|
|
147
154
|
foldrun approvals every gate waiting on a person, with its question (--to <workspace> for one)
|
|
155
|
+
foldrun insights recurring problems found in the runs, with what they cost and what to do (--days, --to, --json)
|
|
156
|
+
foldrun issues what went wrong, grouped, open until resolved (--to <workspace>, --all, --status, --json)
|
|
157
|
+
foldrun issues show|ack|snooze|resolve|reopen <id> one issue — snooze --for 1d or --times 5, --note "…"
|
|
158
|
+
foldrun issues doctor <id> the run doctor on the issue's newest run, in its workspace: why, and the fix as diffs, saved on yes (--yes, --dry-run, --json)
|
|
148
159
|
foldrun approve <run-id> release a waiting gate — asks first, --yes means it, --note "…" steers the step; --payload releases a wait: event step
|
|
149
160
|
foldrun reject <run-id> refuse one, with --note as the reason
|
|
150
161
|
foldrun stop <run-id> kill a run in flight — asks first, --yes means it
|
|
151
162
|
foldrun stop --status running [--since 2h] [--until 1h] [--flow f] every run a filter matches (runs/bulk): lists them, asks, stops only those (--dry-run, --to)
|
|
152
163
|
foldrun answer <run-id> "…" answer the question an agent is asking mid-step (--option <n> picks a choice)
|
|
153
164
|
foldrun message <run-id> "…" say something to a running agent — it hears it after its next tool call
|
|
154
|
-
foldrun rerun <run-id> run it again from a step (--from <n>) or from an agent's step (--agent <name>); --wait
|
|
165
|
+
foldrun rerun <run-id> run it again from a step (--from <n>) or from an agent's step (--agent <name>); --instruction "…" tells that step something else this once; --wait
|
|
155
166
|
foldrun rerun --status failed [--since 24h] [--until 2h] [--flow f] rerun every run a filter matches, from --from <n> (default 1) — lists, asks (--dry-run, --to)
|
|
156
167
|
foldrun observe where the account fails, retries and spends, per workspace (--to <workspace> in full, --since <days>, --json)
|
|
157
168
|
foldrun usage what the account consumed, and what it was charged week by week (--days <n>, --json)
|
|
@@ -172,10 +183,10 @@ const HELP = `foldrun — agents are just folders
|
|
|
172
183
|
foldrun billing card Stripe's page for saving a card for auto top-up (owner only)
|
|
173
184
|
foldrun billing credit <usd> put credit on the balance by hand (--note "…") — only on an install without Stripe (self-hosted, dev); refused once billing is live
|
|
174
185
|
foldrun gallery the platform's built-in tools (web) and whether you keep your own copy — also pull (a copy for offline runs), upgrade <tool>
|
|
175
|
-
foldrun account the account's defaults — also set <key> <value>, clear <key
|
|
186
|
+
foldrun account the account's defaults — also set <key> <value>, clear <key> (timezone, notify, budget, concurrency, deploy_block none|secrets|security, network_enforce on|off), pause [--reason] [--stop] / resume (the kill switch), telemetry [set <url> --header name=\${SECRET} | clear | test] (runs as OpenTelemetry traces), providers (--check) (singular; accounts lists logins)
|
|
176
187
|
foldrun account export everything the platform holds about the account as one JSON file (owner only; --file <path>, --force)
|
|
177
188
|
foldrun account model the account's own model key — set <provider> --key <api key> (--key - reads stdin; custom --base-url --format), remove
|
|
178
|
-
foldrun workspace one deployed workspace's settings (--to) — also set <name|description|timezone|budget|notify> <value>, clear <key
|
|
189
|
+
foldrun workspace one deployed workspace's settings (--to) — also set <name|description|timezone|budget|notify|network_enforce> <value>, clear <key>, pause [--reason] [--stop] / resume (the kill switch)
|
|
179
190
|
foldrun workspace vocabulary the names the dashboard's editor completes in a workspace: agents, flows, skills, tools, secrets, scripts, types, documents (--to, --json)
|
|
180
191
|
foldrun notify test send one test notification from a workspace and say what happened (--to <workspace>)
|
|
181
192
|
foldrun webhooks deliveries the notify: webhooks a workspace sent, newest first — status, attempts, last answer (--failed, --status <s>, --event <e>, --limit, --offset, --to)
|
|
@@ -293,7 +304,10 @@ Options
|
|
|
293
304
|
--step <n> approve, reject: decide only that step; default is every step that is waiting
|
|
294
305
|
--payload <json|text|@f> approve: release a step waiting on wait: event with this body, recorded as you
|
|
295
306
|
--note "<text>" approve, reject: guidance the agent reads — or the reason for a refusal; billing credit: the ledger line's note
|
|
296
|
-
--
|
|
307
|
+
--stale [days] agent ls: only agents not run in that many days (never run included); bare, those past alerts: stale_days
|
|
308
|
+
--reason "<text>" account pause, workspace pause: why — shown to everyone who opens it
|
|
309
|
+
--stop account pause, workspace pause: also stop every run going now (asks; --yes to mean it)
|
|
310
|
+
--yes approve, stop, rerun (bulk), pause --stop, flow rotate-hook, flow draft, assistant, history restore, restore <ws>, import, billing plan, workspace set name, and every rm/revoke/unshare: skip the confirmation, deliberately (required with no terminal)
|
|
297
311
|
--json report: the raw run record instead of the report; observe, usage, api version, changelog, find, preferences, workspace vocabulary, flags, workspaces, workspaces show, agent ls, flow ls, agent import --list, backups requests, team, hook ls: the raw document; version: {cli, core, platform}
|
|
298
312
|
--events <a,b> account set notify: failed, awaiting-approval, completed
|
|
299
313
|
--failed webhooks deliveries: only the ones that gave up
|
|
@@ -365,7 +379,7 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
365
379
|
// `--value` as the account's argument and stored an empty secret; `--force
|
|
366
380
|
// ./dir` swallowed the directory. A flag followed by another flag is also
|
|
367
381
|
// boolean, so an unlisted switch at least does not eat its neighbour.
|
|
368
|
-
const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list", "library", "clear", "security", "changed", "write-baseline", "no-baseline", "rules"]);
|
|
382
|
+
const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list", "library", "clear", "security", "changed", "write-baseline", "no-baseline", "rules", "calls", "tree", "tool-error", "stop"]);
|
|
369
383
|
// Flags said more than once collect into a list: `--tag a --tag b`.
|
|
370
384
|
const REPEATABLE = new Set(["tag", "expect", "header", "site", "add", "remove", "rule"]);
|
|
371
385
|
// `--wait` is a switch everywhere but `flow add <flow> wait`, where it takes
|
package/docs/agents.md
CHANGED
|
@@ -29,6 +29,16 @@ and new posts. Write the digest to outputs/digest.md.
|
|
|
29
29
|
|---|---|
|
|
30
30
|
| `name` | kebab-case, unique in the account. The agent's identity — the folder must match |
|
|
31
31
|
| `description` | what it does *and when to use it*. Other agents read this when consulting; people read it in the dashboard |
|
|
32
|
+
| `owner` | the person who answers for it — an email or a name. Unset, it is the workspace `AGENTS.md`'s `owner:`, then the account's. Shown on the agent list, the agent's page and `foldrun agent ls` |
|
|
33
|
+
|
|
34
|
+
### Who owns it, and whether anyone still uses it
|
|
35
|
+
|
|
36
|
+
`owner:` names the person who answers for the agent. The agent list shows it
|
|
37
|
+
beside **last run** — when its latest step started — so an agent nobody has
|
|
38
|
+
used in months is visible, and `alerts: stale_days` in `AGENTS.md` tells
|
|
39
|
+
`notify:` about one ([Notifications](notifications#thresholds-alerts)).
|
|
40
|
+
An idle agent still holds every tool and secret it was granted: retire it
|
|
41
|
+
by deleting its folder and redeploying.
|
|
32
42
|
|
|
33
43
|
Only `name` and the body are required. Every other field widens or narrows
|
|
34
44
|
what the agent can reach, and the default is narrow.
|
|
@@ -63,8 +73,8 @@ the point of the week; a step that weighs two arguments and picks one is not.
|
|
|
63
73
|
| `hooks` | your own code at every tool call, step start and step end — refuse, rewrite, add context — the same on every model ([Hooks](hooks)). Also in a workspace's or the account's `AGENTS.md` for every agent under it |
|
|
64
74
|
| `apis` | an HTTP API declared inline, as one tool |
|
|
65
75
|
| `mcpServers` | an MCP server declared inline |
|
|
66
|
-
| `secrets` | vault entries its tools may use — names only, never values |
|
|
67
|
-
| `limits` | the most calls one step may make — per tool, per web action, or in all: `limits: {web.search: 40, crm: 20, calls: 300}
|
|
76
|
+
| `secrets` | vault entries its tools may use — names only, never values. The names the step's own model call uses (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_CUSTOM_HEADERS`, the `ANTHROPIC_*_MODEL` tiers, `CLAUDE_CODE_OAUTH_TOKEN`, `FOLDRUN_MODEL_KEY`) never replace the model's own settings, and `foldrun check` warns: script and API tools and `verify:` get the declared value under that name, but the agent's own shell sees the model's — give it another name (`MY_ANTHROPIC_KEY`), or run the model on it with `provider:` |
|
|
77
|
+
| `limits` | the most calls one step may make — per tool, per web action, or in all: `limits: {web.search: 40, crm: 20, calls: 300, repeat: 5}` (`repeat`: the same call with the same input). A call past a limit is refused, never run ([Tools](tools#limits--how-many-calls-a-step-may-make)) |
|
|
68
78
|
| `network` | which sites its sandbox may reach: `open`, `trusted` or `none`, or a list of sites added to the trusted list — `network: [api.hubapi.com]`. Unset, an agent that may fetch or browse the web is `open` and every other `trusted`. Also in a workspace's or the account's `AGENTS.md` ([Network](network)) |
|
|
69
79
|
| `permissionMode` | `plan` makes the run read-only, whatever else was granted |
|
|
70
80
|
| `web` | the `web` tool's settings: `actions:` — which of its eight it may use (all when absent) — and who does each, `web: {search: brave, fetch: jina, browse: browserbase, crawl: firecrawl}`, with the key named in the vault. Unset is foldrun's own. `search:` also takes a block of the own engine's settings — engines, categories, safesearch, plugins ([Tools](tools#tuning-the-accounts-own-engine)); `browse:` a block of the browser's ([below](#webbrowse--which-browser-and-how-it-presents-itself)), and `browse:` alone cascades from `AGENTS.md`. The providers and their keys: [Tools](tools#reaching-the-web). A per-action key outside the block is not read — `check` errors on it |
|
|
@@ -82,6 +92,7 @@ only Web takes a provider:
|
|
|
82
92
|
| | `history` | `recall_runs()` and `read_run(id)` — the workspace's last thirty finished runs | — |
|
|
83
93
|
| | `desks` | `recall_desk_runs()` and `read_desk_run(id)` — the same, across the account's *other* workspaces: ten recent runs each, named per line, up to 100 runs in all. Run records, not files: the way a digest agent reads what every desk concluded this week | — |
|
|
84
94
|
| **People** | `ask` | `ask_person(question, options?)` — asks the person running the desk mid-step and waits for the answer ([below](#asking-a-person-mid-step)) | — |
|
|
95
|
+
| | `peers` | `peers_list()` and `peers_message(to, text)` — the other steps running beside it in its parallel group, and a message to one of them (or all), which reaches that agent after its next tool call ([below](#asking-a-colleague-beside-you)) | — |
|
|
85
96
|
| **Installing** | `install` — every agent has it | `install(manager, packages)` — installs a missing program or library mid-step: Debian packages (`apt`), Python (`pip`), Node (`npm`) ([below](#installing-what-a-step-needs)) | `disallowedTools: [install]` removes it |
|
|
86
97
|
| **Colleagues** | `agents:` · `subagents:` | not in `tools:` — their own fields: consult a colleague for an answer, or delegate a job to one that works with its own tools ([consult](#consulting-colleagues), [delegate](#delegating-to-a-sub-agent)) | — |
|
|
87
98
|
|
|
@@ -116,6 +127,45 @@ Every question, answer and delivered message is on the step's trace and its
|
|
|
116
127
|
record. A local `foldrun run` has no inbox; there `ask_person` asks on the
|
|
117
128
|
terminal when there is one.
|
|
118
129
|
|
|
130
|
+
### Asking a colleague beside you
|
|
131
|
+
|
|
132
|
+
`tools: [peers]` gives the agent `peers_list` and `peers_message`. Steps that
|
|
133
|
+
share a number in a flow run in parallel, and until this they ran blind to
|
|
134
|
+
each other: a writer that needed the image-maker's file name guessed it, and
|
|
135
|
+
an image-maker that found the plan's subject unusable had no one to tell.
|
|
136
|
+
Now a step can ask the colleague beside it for what it has, in the moment,
|
|
137
|
+
instead of guessing, failing, or stopping for a person.
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
tools: [read, write, peers]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`peers_list()` names the other steps of the group — each agent, its step
|
|
144
|
+
number and the first line of its instruction — as they were when the group
|
|
145
|
+
launched. `peers_message(to, text)` leaves a message in that colleague's own
|
|
146
|
+
inbox, the same inbox a person's message uses, signed with the sender's
|
|
147
|
+
agent name; `to: all` reaches every sibling. It reaches the colleague after
|
|
148
|
+
its next tool call, as context, and the colleague answers the same way, so
|
|
149
|
+
nothing waits: the sender keeps working and reads what arrives after its own
|
|
150
|
+
next call. A message is a request or a fact, never a hand-over — the
|
|
151
|
+
receiving step's tools and instruction do not change. A model writing
|
|
152
|
+
without calling tools hears a message at the next call it makes; a sibling
|
|
153
|
+
that has already finished reads nothing, and the sender is told nobody
|
|
154
|
+
received it.
|
|
155
|
+
|
|
156
|
+
The prompt tells the agent who runs beside it this run, so a step that
|
|
157
|
+
happens to run alone knows not to wait for anyone. Each colleague has an
|
|
158
|
+
address, `agent#step` (the step as the flow numbers it), and a message is
|
|
159
|
+
signed with the sender's; a fan-out's instances share one agent name, so the
|
|
160
|
+
bare name reaches every instance and the address reaches one. A sibling
|
|
161
|
+
without `peers` of its own still receives: the inbox is every step's, only
|
|
162
|
+
the sending is the tool's.
|
|
163
|
+
|
|
164
|
+
Every message, sent or received, is on the step's trace and on its record
|
|
165
|
+
(`messages`, with `from` or `to`), and the run page shows the conversation
|
|
166
|
+
under the step beside the person's own. A local `foldrun run` has the same
|
|
167
|
+
line between siblings, in process; only a person cannot write in there.
|
|
168
|
+
|
|
119
169
|
**Retired**, still granted so a deployed agent keeps running, and each an
|
|
120
170
|
error in `foldrun check` naming what to write instead: `files` and `bash`
|
|
121
171
|
(now `write` and `code`). The web is `web` alone; Anthropic's `WebSearch` and
|
|
@@ -530,6 +580,8 @@ from here), `notify:`, `budget:` (a cap in USD over
|
|
|
530
580
|
a month — `60` — or a day or week — `60/day`, `60/week`; unset is no limit),
|
|
531
581
|
`concurrency:` (in the account's file: how many of its runs go at once — the
|
|
532
582
|
next one waits its turn; see [Budgets and billing](budgets-and-billing)),
|
|
583
|
+
`deploy_block:` (`none`, the default, `secrets` or `security` — what the
|
|
584
|
+
scanner's findings do to a deploy; see [Checks](checks#blocking-a-deploy)),
|
|
533
585
|
`limits:` (per-step call limits, as defaults — the one key that merges: an
|
|
534
586
|
agent's `limits: {web.search: 40}` overrides that key and keeps the rest),
|
|
535
587
|
`web.browse:` (see above),
|
package/docs/api.md
CHANGED
|
@@ -66,15 +66,16 @@ api workspaces/$WS/flows/publish/run -X POST # start a flow
|
|
|
66
66
|
| Route | Methods | |
|
|
67
67
|
|---|---|---|
|
|
68
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`). A POST to a name that exists deploys over it; add `ifAbsent: true` to create only — a name that exists is then `409` and nothing changes, decided atomically (the assistant's Create workspace and `foldrun assistant` send it) |
|
|
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 |
|
|
69
|
+
| `/api/workspaces/<ws>` | GET, PATCH, DELETE | read; rename or re-describe (`name`, `description`), or set its defaults (`timezone`, `budget`, `notify`, `network_enforce` — null clears one, edited in AGENTS.md in place); delete |
|
|
70
|
+
| `/api/workspaces/<ws>/pause` | GET, POST, DELETE | the workspace's kill switch. GET `{ paused, account, workspace }` — what holds runs here (`paused` is the one that applies, `scope` saying whose), each `{ at, by, reason? }` or null. POST `{ reason?, stop?: true }` pauses: every start here — schedule, webhook, chained flow, a person's Run, a rerun — is refused `423` with who paused it, and a refused trigger is logged as `halted`; A run queued before the pause and not yet begun waits until the resume. `stop: true` also stops every running, queued and parked run (needs `run:start`) and answers `stopped` and `failed`. DELETE resumes. Needs `workspace:manage`. The account's pause holds here too and only the account's DELETE lifts it; an operator's suspension is separate and neither lifts it |
|
|
70
71
|
| `/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
|
-
| `/api/workspaces/<ws>/check` | GET | what `foldrun check` says about the deployed copy, as findings with rule ids — what a deploy refuses or warns about, and the scanner's secret, hidden-text, agent-risk and supply-chain rules ([Checks](checks)). `?security=1` keeps the security rules, `?rule=<id or category>` (repeatable) one; `{ counts, findings: [{ rule, level, file, line, message, fingerprint }], rules }`. Read-only, no model call |
|
|
72
|
+
| `/api/workspaces/<ws>/check` | GET | what `foldrun check` says about the deployed copy, as findings with rule ids — what a deploy refuses or warns about, and the scanner's secret, hidden-text, agent-risk and supply-chain rules ([Checks](checks)). `?security=1` keeps the security rules, `?rule=<id or category>` (repeatable) one, `?online=1` adds the network checks (known vulnerabilities, remote MCP tool text); `{ counts, findings: [{ rule, level, file, line, message, fingerprint }], rules, online?: { problems, sources }, rescan: { checkedAt, findings, pending, problems } | null }`. Read-only, no model call |
|
|
72
73
|
|
|
73
74
|
## Source — the files themselves
|
|
74
75
|
|
|
75
76
|
| Route | Methods | |
|
|
76
77
|
|---|---|---|
|
|
77
|
-
| `/api/workspaces/<ws>/source?path=` | GET, PUT, PATCH, DELETE | GET: one file (`{ path, content, sha256 }`), or the tree with no `path`. PUT `{path, content, message?, ifMatch?}` writes one file (a revision, `message` its note); with `ifMatch` — the `sha256` a GET returned — it is refused with `409` and nothing written if the file has changed since; `ifMatch: null` says the caller saw no file there, and is refused the same way once one exists (History's restore and the AI draft's save send it, the draft's new files as `null`). PATCH `{from, to}` moves one. DELETE `{path}` removes a file or folder. The body carries the path on writes — a `?path=` there is a `400` |
|
|
78
|
+
| `/api/workspaces/<ws>/source?path=` | GET, PUT, PATCH, DELETE | GET: one file (`{ path, content, sha256 }`), or the tree with no `path`. PUT `{path, content, message?, ifMatch?, check?}` writes one file (a revision, `message` its note) and answers `{ ok, path, sha256, issues }`, the deploy's checks for that file as saved. With `check: true` (what the dashboard editor sends) a file with an error a deploy would refuse is refused with `422` and the `issues`, nothing written; warnings never refuse; with `ifMatch` — the `sha256` a GET returned — it is refused with `409` and nothing written if the file has changed since; `ifMatch: null` says the caller saw no file there, and is refused the same way once one exists (History's restore and the AI draft's save send it, the draft's new files as `null`). PATCH `{from, to}` moves one. DELETE `{path}` removes a file or folder. The body carries the path on writes — a `?path=` there is a `400` |
|
|
78
79
|
| | PUT | write (`path`, `content`, optional `message` — recorded on the revision, as `foldrun source put --message` sends it) |
|
|
79
80
|
| | PATCH | move (`from`, `to`) |
|
|
80
81
|
| | DELETE | remove (`path`) |
|
|
@@ -171,7 +172,7 @@ history with each message.
|
|
|
171
172
|
|
|
172
173
|
| Route | Methods | |
|
|
173
174
|
|---|---|---|
|
|
174
|
-
| `/api/workspaces/<ws>/agents` | GET, POST | list; create (`name`) |
|
|
175
|
+
| `/api/workspaces/<ws>/agents` | GET, POST | list — each agent's frontmatter as read, `owner` and `ownerFrom` (its own `owner:`, else the workspace's, else the account's), `lastRunAt` (when its latest step that ran started, from the runs kept; null for none) and `stale` (past `alerts: stale_days`); create (`name`) |
|
|
175
176
|
| `/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 } }`, or `{ network }` — the sites its sandbox may reach: `"open"`, `"trusted"`, `"none"`, a list of sites, or `{ level, allow, defaults }`; `null` removes the line so it inherits ([Network](network)). A `network` nobody could read is a 400 that says why. Only that key's lines change — comments, key order and prose stay. Answers `{ ok, changed }`; 404 for an unknown agent. `workspace:write` |
|
|
176
177
|
| `/api/workspaces/<ws>/agents/<agent>/run` | POST | run one agent alone (`task`; `test: true` for a test run) |
|
|
177
178
|
| `/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 |
|
|
@@ -190,23 +191,26 @@ paying for the steps around it.
|
|
|
190
191
|
|
|
191
192
|
| Route | Methods | |
|
|
192
193
|
|---|---|---|
|
|
193
|
-
| `/api/workspaces/<ws>/runs?limit=` | GET | `{ runs: [{ id, flow, status, startedAt, finishedAt, ... }] }` — off the run index (see [Runs](runs#the-runs-list)); a finished run's steps carry `agent`, `status`, `costUsd` and `tokens` (`{ input, output, cacheRead, cacheWrite }`, see [Runs](runs#the-record)), and the run its summed `tokens` — not their reply or trace |
|
|
194
|
+
| `/api/workspaces/<ws>/runs?limit=` | GET | `{ runs: [{ id, flow, status, verdict, outcome, startedAt, finishedAt, ... }] }`; search (see [Runs](runs#searching-runs)) with `tool` (a step called a tool whose name contains it), `tool_error=1`, `agent`, `model` (contains), `min_tokens` (`50k`), `min_secs` — `limit` counts the matches — `verdict` is the word the summary leads with (`GOOD`, `QUIET`, `BLOCKED`, `BAD` or null) and `outcome` the one word every page shows (`running`, `queued`, `waiting`, `good`, `done`, `quiet`, `blocked`, `bad`, `failed`, `stopped`); `failure` is `{ agent, reason }` for a run that failed (null otherwise), `chargedUsd` is what the account was charged for the run (one credit is one cent), null until the ledger settles it, while `costUsd` is the provider's token cost — off the run index (see [Runs](runs#the-runs-list)); a finished run's steps carry `agent`, `status`, `costUsd` and `tokens` (`{ input, output, cacheRead, cacheWrite }`, see [Runs](runs#the-record)), and the run its summed `tokens` — not their reply or trace |
|
|
195
|
+
| `/api/workspaces/<ws>/runs/<id>/calls/<call>` | GET | one tool call's recorded content (see [Runs](runs#what-a-run-records)): `{ call: { id, tool, input, output, inputChars, outputChars, truncated, inputCut, outputCut } }` — what it was given and what it gave back, secrets masked, each at most 65,536 characters; `inputChars`/`outputChars` are the whole length, `inputCut`/`outputCut` say which part was cut (`truncated`: either). `<call>` is the tool event's `call`; an event with `captured: true` has content here. 404 when nothing was recorded: the workspace says `capture: metadata`, or the run is from before 19 Oct 2026. A run's steps also carry `calls`, each model call (`n`, `at`, `ms`, `model`, `stop`, `tokens`, `tools`, and `subagent` / `attempt` when a sub-agent or a later attempt made it), and the run its `revision` (the workspace's latest revision when a worker picked it up — a deploy's commit, or the id an edit through `/source` was saved as) and `drivenAt` (when that was) |
|
|
196
|
+
| `/api/workspaces/<ws>/runs/<id>/trace` | GET | the run as one tree (see [Runs](runs#the-trace-tree)): `{ tree, failure }`. Each span is `{ id, kind, label, start, end, status, children, … }`, where `kind` is `run`, `step`, `model` (one model call: `model`, `stop`, `tokens`), `tool` (`call`, `captured` when its input and output were recorded) or `error` (`text`); `start`/`end` are epoch milliseconds and `status` is `ok`, `error`, `running`, `waiting`, `skipped` or `pending`. A tool call sits under the model call that asked for it. `failure` is the ids from the root to the first thing that went wrong, which the run page opens on; `[]` when nothing did |
|
|
197
|
+
| `/api/workspaces/<ws>/runs/<id>/compare?with=` | GET | this run beside another of the same flow (see [Runs](runs#compare-with-the-last-good-run)): `{ comparison: { base, run, revisionChanged, changedFiles, steps }, chosen }`. Each step is `{ step, agent, base, run, changes }`: both sides' status, instruction, model, seconds, tokens, model calls, tool counts, tool errors, first errors and first line said, and `changes` in words (`status: completed → failed`, `new error: …`, `instruction changed`, `tools: +web ×3`, `took 12s → 48s`, …). `changedFiles` lists the files changed between the revisions the two ran (null when one is unknown). `with` names the other run; unasked it is the newest earlier run of the flow that finished well, else the run this one re-ran (`chosen` says which) |
|
|
194
198
|
| `/api/workspaces/<ws>/runs/<id>` | GET, DELETE | the whole run: every step, its output, cost, timing, the runner `image` it ran on and — for a step that browsed through the account's pod from slim — its `browserPod` reconnects and fallback ([runs](runs#the-record)). `?wait=true&timeout=<s>` instead blocks until the run stops (or `timeout`, default 300, max 900) and answers the verdict the run routes give — `200` completed, `500` failed with the record, `202` still going (`timedOut`) or parked on a person (`awaitingApproval`). Ask in short pieces: whatever sits in front of the platform cuts a request that stays silent too long (Cloudflare at 100 s, an AWS ALB at 60 s), and a flow can run for many minutes — the CLI's `--wait` asks 25 s at a time and comes back here on every `202` |
|
|
195
199
|
| `/api/workspaces/<ws>/runs/<id>/stream` | GET | server-sent events, live |
|
|
196
200
|
| `/api/workspaces/<ws>/runs/<id>/stop` | POST | destroys the sandbox, not just the record |
|
|
197
|
-
| `/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 |
|
|
201
|
+
| `/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). `instruction` tells that step something else this once, in place of the flow's (the flow file is not changed); the new run's `rerunOf` says `{ run, step, instruction }`. Takes `?wait=true&timeout=` like a flow start |
|
|
198
202
|
| `/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 |
|
|
199
203
|
| `/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 |
|
|
200
204
|
| `/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 |
|
|
201
205
|
| `/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}` |
|
|
202
206
|
| `/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 |
|
|
203
207
|
| `/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 |
|
|
204
|
-
| `/api/workspaces/<ws>/runs/<id>/promote` | POST | save a
|
|
205
|
-
| `/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) |
|
|
208
|
+
| `/api/workspaces/<ws>/runs/<id>/promote` | POST | save a finished 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. A run that went wrong (failed, BLOCKED or BAD) needs `should` — what it should have done, the judge's claim for the case — or `expect`: its own answer is not the one to keep. Returns `{ ok, file, created, caseName }` |
|
|
209
|
+
| `/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, a workspace or account paused by a person (`halted`), 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) |
|
|
206
210
|
| `/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 |
|
|
207
211
|
| `/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 |
|
|
208
212
|
| `/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 |
|
|
209
|
-
| `/api/workspaces/<ws>/observe?days=` | GET | the observability report the Observe page renders — per-agent failures, retries, tokens
|
|
213
|
+
| `/api/workspaces/<ws>/observe?days=` | GET | the observability report the Observe page renders — `tally` (the shared outcome counts) and `chargedUsd` beside `costUsd` on the report, each flow, day and agent (what was charged, the ledger's line or list price until settled, versus the provider's token cost); per-agent failures, retries, tokens, `cacheHitRate`, `turns` and `systemPromptChars` (each `{ n, p50, p95, max }`), per-tool calls/errors/latency and `resultSize` (characters per result, same shape), `resultChars` (their sum) and `heavyCalls` (results over 10,000 characters), per-flow duration, day series, recent failures, and `startup` — one row per start-up phase in order, `{ phase, took, at }`, each `{ n, p50, p95, max }` in seconds (`took` is the phase's own share, `at` when it ended counted from the step's start; percentiles need five samples). `days` 1–365, default 30 (`since=`, its older name, still works — deprecated) |
|
|
210
214
|
| `/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 |
|
|
211
215
|
| `/api/workspaces/<ws>/restore` | POST | put the workspace's source back as it was at a point in its history. `to`: a revision id (`r-…`), the commit a deploy named (or its prefix), an ISO date/time (the last revision at or before it) or an age (`90m`, `12h`, `3d`, `2w`) — every revision since 17 Oct 2026 holds a snapshot of the whole source. `{ to, dryRun: true }` answers `{ to: { id, 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 revision's snapshot as a new revision (undo: restore to `from`; `to.sha` is the same id as `to.id`, kept from when revisions were git commits), 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` |
|
|
212
216
|
|
|
@@ -324,7 +328,7 @@ Reads return which secrets exist, never their values.
|
|
|
324
328
|
|
|
325
329
|
| Route | Methods | |
|
|
326
330
|
|---|---|---|
|
|
327
|
-
| `/api/keys` | GET, POST, DELETE | API keys — an admin's (`keys:manage`) to list, mint and revoke. POST `label`, `role` (viewer \| editor \| admin, never above your own, default editor), `workspaces` (names, or `null` for all — never wider than your own scope); `expires` (`30d`, `90d`, `365d`, any number of days up to 3650, or `never` — the default; anything else is a 400, never a key that lives forever); returns `{ ok, key, id, prefix, role, workspaces, expiresAt }`, and the key is shown once. GET lists each key with `createdAt`, `createdBy`, `expiresAt`, `lastUsedAt`, `lastIp` and `replacedBy` (the id of the key a rotation replaced it with; `null` until then) — the last-use pair written at most once a minute per key, the address cut to its /24 (IPv4) or /48 (IPv6). A key past its `expiresAt` is refused with 401 `API key expired on <date>`; the owner is emailed seven days before. Mint, revoke and rotate are written to the audit log. POST with `workspace` or `access` is refused (400): those minted deploy keys for the platform's own git, which went on 17 Oct 2026 — a deploy key minted before is refused everywhere (403). A key for one workspace is `workspaces: ["<ws>"]`. DELETE `id`. `?tenant=` / body `tenant` is read only on an install with no users yet (first-run bootstrap) |
|
|
331
|
+
| `/api/keys` | GET, POST, DELETE | API keys — an admin's (`keys:manage`) to list, mint and revoke; any key may revoke itself (DELETE with its own `id`, audited as self — what `foldrun logout` does). POST `label`, `role` (viewer \| editor \| admin, never above your own, default editor), `workspaces` (names, or `null` for all — never wider than your own scope); `expires` (`30d`, `90d`, `365d`, any number of days up to 3650, or `never` — the default; anything else is a 400, never a key that lives forever); returns `{ ok, key, id, prefix, role, workspaces, expiresAt }`, and the key is shown once. GET lists each key with `createdAt`, `createdBy`, `expiresAt`, `lastUsedAt`, `lastIp` and `replacedBy` (the id of the key a rotation replaced it with; `null` until then) — the last-use pair written at most once a minute per key, the address cut to its /24 (IPv4) or /48 (IPv6). A key past its `expiresAt` is refused with 401 `API key expired on <date>`; the owner is emailed seven days before. Mint, revoke and rotate are written to the audit log. POST with `workspace` or `access` is refused (400): those minted deploy keys for the platform's own git, which went on 17 Oct 2026 — a deploy key minted before is refused everywhere (403). A key for one workspace is `workspaces: ["<ws>"]`. DELETE `id`. `?tenant=` / body `tenant` is read only on an install with no users yet (first-run bootstrap) |
|
|
328
332
|
| `/api/keys/<id>/rotate` | POST | a new key with the old one's label, role, workspaces and deploy scope — and, when it had an end, the same lifetime from today — returned once like a mint (`{ ok, key, id, prefix, role, workspaces, expiresAt, replaced: { id, endsAt } }`). `grace`: `0` (default — the old key is revoked at once), or a duration up to `7d` such as `1h` or `24h`, during which the old key still works and after which it is refused as expired. A key is rotated once: rotating a key already replaced is 409 `this key was already rotated to <prefix>…; rotate that one` — and someone who lost the new key's secret rotates the NEW key, which mints another and ends the lost one (rotating the old one again would leave the lost key working). Two rotations of one key at once mint one key. `keys:manage`; a key outside the caller's scope is 404. Audited as `apikey.rotated` |
|
|
329
333
|
| `/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 |
|
|
330
334
|
| `/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 |
|
|
@@ -334,14 +338,19 @@ Reads return which secrets exist, never their values.
|
|
|
334
338
|
| `/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 |
|
|
335
339
|
| `/api/usage` | GET | what has been spent, by meter; each bucket's tokens are `inputTokens` (all input), `outputTokens`, `cacheReadTokens` and `cacheWriteTokens` (the part of the input the prompt cache served and stored), and `cacheHitRate` — reads over `measuredInputTokens`, the input of steps that report the split, `null` when none do. 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 |
|
|
336
340
|
| `/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` |
|
|
341
|
+
| `/api/insights?days=&workspace=` | GET | recurring problems found in the runs of the window (see [Runs](runs#insights)): `{ days, insights: [{ id, kind, workspace, agent, tool?, title, why, fix, credits, steps, evidence: [{ workspace, runId, step }] }] }`. `kind` is `cache` (an agent's big prompt mostly uncached), `big-result` (a tool result over 20k characters sent again on every later model call), `retry-loop` (a tool erroring three or more times in a step) or `timeout` (steps using 80% or more of their `timeout:`); `credits` is about what it cost in the window, null when it is not money. `days` 1–30, 7 unasked. A key scoped to some workspaces sees theirs |
|
|
342
|
+
| `/api/issues?status=&workspace=` | GET | the account's issues: what went wrong, grouped and kept open until a person resolves it (see [Runs](runs#issues)). One issue is one desk's flow failing at one agent, or ending BLOCKED or BAD, or one platform cause across desks. Each carries `id`, `kind` (`platform`, `failed`, `blocked`, `bad`, `missed` — a scheduled fire that started no run), `status` (`open`, `acknowledged`, `snoozed`, `resolved`), `regressed` and `sinceRevision` (it was resolved and came back, since the revision its new run ran), `snoozedFor`, `workspace`/`workspaces`, `what`, `agent`, `reason`, `action` (what to do), `firstSeen`, `lastSeen`, `count`, the newest `runs`, `recoveredAt` (the flow's newest good run since — it closes nothing), `acknowledged`, `snoozedUntil`, `resolved` and the `history` of what people did. `status` is `active` unasked (open + acknowledged), or `open`, `acknowledged`, `snoozed`, `resolved`, `all`; `workspace` keeps the ones that hit it. A key scoped to some workspaces sees the issues of those |
|
|
343
|
+
| `/api/issues/<id>` | GET POST | one issue; POST acts on it: `{ action: "acknowledge" | "snooze" | "resolve" | "reopen", for?: "30m" | "4h" | "1d" | "1w", until?: ISO, times?: n, note? }` (editor or above), kept in its history with who did it, and answers `{ issue }` as it now stands. A snooze needs `for`, a future `until`, or `times` (until it happens that many more times; with a time too, whichever comes first). A platform issue is the account's: closing it needs access to every workspace. A run that goes wrong the same way after a resolve opens the issue again |
|
|
337
344
|
| `/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 |
|
|
345
|
+
| `/api/account/telemetry/test` | POST | send one made-up run as an OpenTelemetry trace to the collector `telemetry:` names — the account's, or `{ workspace }`'s — with its header secrets filled from the vault; `{ ok, status?, error?, spans?, endpoint }`. `409` when no `telemetry:` is set. Needs `workspace:manage` |
|
|
346
|
+
| `/api/account/pause` | GET, POST, DELETE | the account's kill switch: the same as a workspace's `/pause`, for every workspace at once. GET `{ paused, account, workspaces }` (`workspaces`: those paused on their own). POST `{ reason?, stop?: true }`, DELETE resumes the account — a workspace paused on its own stays paused. Needs `workspace:manage` and access to every workspace |
|
|
338
347
|
| `/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 |
|
|
339
348
|
| `/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) |
|
|
340
349
|
| `/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 |
|
|
341
350
|
| `/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 API-key requests without a `Foldrun-Version` header get (null: always current); the dashboard always gets current. `keys:manage`. See [api-usage](api-usage.md) |
|
|
342
351
|
| `/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) |
|
|
343
352
|
| `/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 |
|
|
344
|
-
| `/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 |
|
|
353
|
+
| `/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 }`), `concurrency`, `deploy_block` (`none`, `secrets` or `security` — answered as `deployBlock`), `network_enforce` (answered as `networkEnforce`), `telemetry` (`{ otlp, headers }` — header values are `${SECRET}` names; answered as `{ endpoint, headers }`); PATCH sets the keys sent, null clears one, and edits the file in place |
|
|
345
354
|
| `/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 |
|
|
346
355
|
| `/api/avatars/<kind>/<id>` | GET, PUT, DELETE | a picture for a `user` (their own id), a `workspace` (its name) or the `account` (the tenant). PUT the bytes of a PNG, JPEG or WebP under 1 MB; a person sets their own, an admin a workspace's or the account's. GET is cached for a year — the URL's `?v=` changes with the picture |
|
|
347
356
|
| `/api/search?q=` | GET | find anything in the account by name — workspaces, agents, flows, tools, skills, knowledge, memory, runs (by id, flow or headline); ranked, at most six per kind. The dashboard's Find palette reads it |
|
|
@@ -443,9 +452,9 @@ signed-in person, never by a key — mints an ordinary API key at their role.
|
|
|
443
452
|
| `/api/cli/login/<id>` | GET | `{ status }` — `pending`, `denied`, `expired`, or `approved` with `token`, `account`, `email`, `role`. The key is returned once; the request is gone after |
|
|
444
453
|
| `/api/cli/authorize` | GET, POST | `?code=` describes the request (machine, expiry). POST `{ code, decision }` from a session approves (mints the key, labelled `cli · <hostname>`) or denies — `decision` is `approve` or `deny`, and absent approves; any other word is a `400`. Every role may sign its own CLI in; an owner's key is an admin's |
|
|
445
454
|
| `/api/me` | GET | who is calling: `account`, `owner`, `role`, `workspaces` (null: all), and the `actor` — a person's email, or a key's id, label, prefix and who minted it |
|
|
446
|
-
| `/api/me/onboarding` | GET, PATCH | the account's getting-started steps, each derived from what is really there — `{ steps: [{ id, title, why, href, cli?, done }], done, total, complete, next, dismissed, dismissedAt }`. Steps: `workspace`, `agent` (an agent written or deployed after the workspace was made), `run`, `success` (a completed run), `secret`, `trigger` (a schedule or webhook), `teammate` (a second member), `billing` (hosted, not fully waived: a saved card or a plan), `cli` (any API key or CLI login). PATCH `{ dismissed: true or false }` hides or brings back the dashboard card for the signed-in person (kept in their prefs); a key reads `dismissed: false` and cannot PATCH |
|
|
455
|
+
| `/api/me/onboarding` | GET, PATCH | the account's getting-started steps, each derived from what is really there — `{ steps: [{ id, title, why, href, cli?, done }], done, total, complete, next, dismissed, dismissedAt }`. Steps: `model` (first; only for an account that must bring its own key — done once it has one, or a `provider:` block in the account's AGENTS.md), `workspace`, `agent` (an agent written or deployed after the workspace was made), `run`, `success` (a completed run), `secret`, `trigger` (a schedule or webhook), `teammate` (a second member), `billing` (hosted, not fully waived: a saved card or a plan), `cli` (any API key or CLI login). PATCH `{ dismissed: true or false }` hides or brings back the dashboard card for the signed-in person (kept in their prefs); a key reads `dismissed: false` and cannot PATCH |
|
|
447
456
|
| `/api/me/notifications` | GET, PATCH | what mail you get. GET `{ email, categories: [{ category, label, description, required, why?, perWorkspace, enabled, workspaces: { <ws>: bool } }] }` for `security`, `invites`, `billing`, `account` (required — always sent) and `run-alerts`, `approvals` (per workspace), `low-balance`, `product-updates`. PATCH `{ category, enabled, workspace? }` or `{ preferences: [ … ] }`; answers the same as GET. Turning a required category off is a `409` with the reason; `workspace` on an account-wide category is a `400`; `enabled: true` account-wide clears that category's per-workspace offs. A key acts for the person who minted it |
|
|
448
|
-
| `/api/me/preferences` | GET, PATCH | your own display preferences: `{ theme }` — `system` (the default: follow the computer's light or dark setting), `light` or `dark
|
|
457
|
+
| `/api/me/preferences` | GET, PATCH | your own display preferences: `{ theme, runViews }` — `theme` is `system` (the default: follow the computer's light or dark setting), `light` or `dark`; `runViews` your saved run searches, `[{ name, query }]` (see [Runs](runs#searching-runs)). PATCH `{ theme }` and/or `{ runViews }` (the whole list, at most 20, each query of the runs filters) answers the same; anything else is a `400`. Kept in your prefs, so it follows you to every browser; the dashboard applies it on the server, before the first paint. A key acts for the person who minted it; a key minted by nobody is a `400` |
|
|
449
458
|
| `/api/unsubscribe?token=` | POST | one-click unsubscribe (RFC 8058) — no login, the signed token is the credential. It is the `List-Unsubscribe` URL on every optional mail, beside `List-Unsubscribe-Post: List-Unsubscribe=One-Click`; a mail client POSTs `List-Unsubscribe=One-Click` to it. Turns off the one category (for the one workspace) the token was minted for, for its address. The token may also come as a form field (`token`, and `from=page` to be sent back 303 to the `/unsubscribe` confirmation page) or JSON. `200 { ok, category, workspace }`; `400` no token, `401` forged, `410` expired (180 days; `FOLDRUN_UNSUBSCRIBE_TTL_DAYS`). Never on a GET — `/unsubscribe?token=` is the page that asks first |
|
|
450
459
|
|
|
451
460
|
## OAuth — connecting an agent to a third party
|
package/docs/assistant.md
CHANGED
|
@@ -14,7 +14,7 @@ your browser remembers the choice. Each workspace keeps its own conversation.
|
|
|
14
14
|
|
|
15
15
|
## What it knows
|
|
16
16
|
|
|
17
|
-
Only
|
|
17
|
+
Only three things:
|
|
18
18
|
|
|
19
19
|
- **foldrun's docs**, these pages. It searches them and reads the sections
|
|
20
20
|
it needs before it answers, and it cites what it used as
|
|
@@ -22,14 +22,44 @@ Only two things:
|
|
|
22
22
|
**Read N doc sections** lists them.
|
|
23
23
|
- **This workspace's files**: its `AGENTS.md`, agents, flows, skills,
|
|
24
24
|
tools, hooks, knowledge, evals and scripts, plus the account library and
|
|
25
|
-
the account's `AGENTS.md`. It never reads `state/`, `storage
|
|
25
|
+
the account's `AGENTS.md`. It never reads `state/`, `storage/` or
|
|
26
26
|
secrets.
|
|
27
|
+
- **This workspace's runs**, when you ask about one: each step's status,
|
|
28
|
+
verify, errors and result, and one step in full. Run records are
|
|
29
|
+
redacted as they are written, so no secret value is in them.
|
|
27
30
|
|
|
28
31
|
It has no web access, no shell and no other tools. If the docs do not cover
|
|
29
32
|
something, it says so; it does not guess. Anything that is not about
|
|
30
33
|
building on foldrun gets a one-line refusal. Text inside your files is
|
|
31
34
|
treated as data, never as instructions to it.
|
|
32
35
|
|
|
36
|
+
## The run doctor
|
|
37
|
+
|
|
38
|
+
A run that failed, or ended BAD or BLOCKED, has a **Doctor** button on its
|
|
39
|
+
page, and so does each [issue](runs#issues) under Needs attention and on the
|
|
40
|
+
Issues page, for the issue's newest run. It opens the assistant with "Doctor:
|
|
41
|
+
why did run <id> fail, and how do I fix it?" ready to send, in the workspace
|
|
42
|
+
that run is in — from the account home too. The assistant reads the run, then the
|
|
43
|
+
failing step in full and the files it names, and answers in this order:
|
|
44
|
+
|
|
45
|
+
1. **Cause**, one of four:
|
|
46
|
+
- `platform`: foldrun itself, such as a pod, the worker or a proxy.
|
|
47
|
+
- `check`: the work was done, but a `verify:` graded the wording, not
|
|
48
|
+
the result.
|
|
49
|
+
- `work`: the agent did not do what was asked.
|
|
50
|
+
- `outside`: a provider, a key, a balance, or a site that refused.
|
|
51
|
+
2. **What happened**, quoting the run's lines that show it.
|
|
52
|
+
3. **The fix**. For `check` and `work` it proposes changed files as diffs:
|
|
53
|
+
a verify that checks the artefact, a clearer instruction, an `on-fail:`
|
|
54
|
+
rescuer. For `platform` and `outside` it says who must act.
|
|
55
|
+
4. **Where to rerun from**: the step, or a fresh run.
|
|
56
|
+
|
|
57
|
+
It never proposes a verify that accepts BLOCKED, or removes a check to make
|
|
58
|
+
a run pass. Nothing is saved or rerun by itself: you save the fix, then
|
|
59
|
+
rerun from the run page. From a terminal:
|
|
60
|
+
`foldrun runs doctor <run-id> --to <workspace>`, or `foldrun issues doctor
|
|
61
|
+
<issue-id>` for an issue's newest run.
|
|
62
|
+
|
|
33
63
|
## What it writes
|
|
34
64
|
|
|
35
65
|
Only these paths:
|
|
@@ -145,11 +145,12 @@ cost from what the last one consumed.
|
|
|
145
145
|
|
|
146
146
|
## Prompt caching
|
|
147
147
|
|
|
148
|
-
Every step's model calls are cached by prefix, on
|
|
148
|
+
Every step's model calls are cached by prefix, on the provider's side: a
|
|
149
|
+
request whose opening bytes match one the model has seen reads the model's
|
|
150
|
+
work on them instead of paying for it again. The opening is the agent's
|
|
149
151
|
tools and system prompt, then the conversation so far. Each turn of a step
|
|
150
|
-
re-reads what the turn before sent, and steps of the same agent
|
|
151
|
-
|
|
152
|
-
switched on.
|
|
152
|
+
re-reads what the turn before sent, and steps of the same agent share the
|
|
153
|
+
same opening. Nothing has to be switched on.
|
|
153
154
|
|
|
154
155
|
A read from the cache is priced at a tenth of fresh input, and a write at a
|
|
155
156
|
quarter more; a step's cost uses those rates, both against its `budget:` and
|
|
@@ -159,6 +160,42 @@ Usage and Observe pages, `foldrun report`, `foldrun usage` and
|
|
|
159
160
|
`foldrun observe` show the **cache hit rate**: cache reads over all input,
|
|
160
161
|
counted over the steps that report the split (from 18 Oct 2026).
|
|
161
162
|
|
|
163
|
+
### How the prompt is laid out for it
|
|
164
|
+
|
|
165
|
+
A byte that differs invalidates everything after it, so the system prompt
|
|
166
|
+
is assembled in **layers**, the ones that never change first (from 20 Oct
|
|
167
|
+
2026): the agent's prose, its locale and place, `AGENTS.md`, skills,
|
|
168
|
+
knowledge, scripts, APIs, tools, secrets and the platform's rules; then
|
|
169
|
+
what moves between runs — the memory index, the state files and the clock.
|
|
170
|
+
The prompt is built once per step and cached as one block, so every turn
|
|
171
|
+
of a step re-reads it, and so do the agent's other steps in the same run
|
|
172
|
+
while it is unchanged. When an agent's memory or state changes, its next
|
|
173
|
+
step writes the prompt again; across runs a day apart that costs nothing
|
|
174
|
+
extra, since the cache has expired by then anyway. Each layer's name,
|
|
175
|
+
length and hash ride on the step record as `promptLayers`.
|
|
176
|
+
|
|
177
|
+
A **fan-out** of one agent (`each:`) starts one instance first and holds
|
|
178
|
+
the rest until its first response has begun, then starts them together:
|
|
179
|
+
the cache entry a request writes is readable only once its response has
|
|
180
|
+
begun, and twenty copies started at once would each write the same prefix
|
|
181
|
+
and none read it. It waits for the response, not for a fixed time — each
|
|
182
|
+
copy spends seconds starting its sandbox before it calls the model, and a
|
|
183
|
+
timer would release the rest before there was anything to read. The
|
|
184
|
+
waiting copies say so on their trace (`prompt cache: waiting for the first
|
|
185
|
+
… step's reply`); `parallel:` still caps how many run at once after that.
|
|
186
|
+
On dev, 20 Oct 2026, the waiting copies of a three-way fan-out read 88% of
|
|
187
|
+
their input from the cache, against 19% for the one that went first.
|
|
188
|
+
|
|
189
|
+
### When a step wrote instead of read
|
|
190
|
+
|
|
191
|
+
A step that wrote a fifth or more of its input to the cache gets a
|
|
192
|
+
**prompt cache** line on the run page and in `foldrun report`, saying why:
|
|
193
|
+
`memory, clock changed since step 2` compares its layers with the same
|
|
194
|
+
agent's previous step in the run; `the agent's first step in this run` is
|
|
195
|
+
the write every run pays once; `the same prompt as step 2; the entry had
|
|
196
|
+
expired or was still being written` means nothing in the prompt moved —
|
|
197
|
+
the gap was longer than the cache lives, or two copies raced.
|
|
198
|
+
|
|
162
199
|
The cache keeps a prefix for an hour on a Claude subscription and five
|
|
163
200
|
minutes on an API key. A low rate usually means one of:
|
|
164
201
|
|
|
@@ -170,7 +207,11 @@ minutes on an API key. A low rate usually means one of:
|
|
|
170
207
|
|
|
171
208
|
Providers on the OpenAI format (`provider:` with `format: openai` or
|
|
172
209
|
`responses`) cache on their own side; the share they report comes back as
|
|
173
|
-
`cacheRead` too.
|
|
210
|
+
`cacheRead` too. Where the endpoint routes its cache by a key (OpenAI,
|
|
211
|
+
Mistral), every request of an agent carries the same `prompt_cache_key`,
|
|
212
|
+
one per agent and account, so its requests land where its prefix is. Other
|
|
213
|
+
endpoints get the layered prompt and nothing else; a self-hosted model
|
|
214
|
+
(vLLM, Ollama) caches prefixes in its own memory and benefits the same way.
|
|
174
215
|
|
|
175
216
|
## What each desk costs
|
|
176
217
|
|