foldrun 0.5.0 → 0.7.2
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 +98 -15
- package/bin/foldrun.mjs +331 -47
- package/docs/agents.md +483 -0
- package/docs/api-usage.md +156 -0
- package/docs/api.md +435 -0
- package/docs/backups-and-restore.md +171 -0
- package/docs/budgets-and-billing.md +362 -0
- package/docs/cli.md +428 -0
- package/docs/coding-agents.md +224 -0
- package/docs/deploying.md +77 -0
- package/docs/evals.md +90 -0
- package/docs/faq.md +53 -0
- package/docs/flows.md +670 -0
- package/docs/getting-started.md +141 -0
- package/docs/glossary.md +78 -0
- package/docs/harness.md +128 -0
- package/docs/how-it-works.md +255 -0
- package/docs/introduction.md +47 -0
- package/docs/knowledge-and-memory.md +85 -0
- package/docs/notifications.md +320 -0
- package/docs/providers.md +241 -0
- package/docs/runs.md +238 -0
- package/docs/scheduling.md +103 -0
- package/docs/secrets.md +163 -0
- package/docs/security.md +167 -0
- package/docs/self-hosting.md +65 -0
- package/docs/status.md +73 -0
- package/docs/storage-and-sharing.md +118 -0
- package/docs/team-and-access.md +146 -0
- package/docs/tools.md +1810 -0
- package/docs/troubleshooting.md +155 -0
- package/docs/versions.md +79 -0
- package/docs/workspaces.md +160 -0
- package/package.json +15 -5
- package/src/commands.mjs +6984 -331
- package/src/credentials.mjs +3 -0
- package/src/guide.mjs +172 -0
package/docs/agents.md
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# The agent file
|
|
2
|
+
|
|
3
|
+
`agents/<name>/agent.md` is the only required file an agent has. Frontmatter
|
|
4
|
+
for the machine, body for the model — and the body is the prompt, so write it
|
|
5
|
+
as instructions to a colleague rather than as documentation about one.
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
---
|
|
9
|
+
name: competitor-watcher
|
|
10
|
+
description: Watches competitor sites and drafts a weekly digest.
|
|
11
|
+
model: default
|
|
12
|
+
effort: high
|
|
13
|
+
tools:
|
|
14
|
+
- web
|
|
15
|
+
- write
|
|
16
|
+
- [[price-tracker]]
|
|
17
|
+
secrets: [SLACK_WEBHOOK_TOKEN]
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
You watch our competitors and produce a weekly digest.
|
|
21
|
+
|
|
22
|
+
Check each site listed in [[competitors]]. Note pricing changes, new features
|
|
23
|
+
and new posts. Write the digest to outputs/digest.md.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Identity
|
|
27
|
+
|
|
28
|
+
| field | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `name` | kebab-case, unique in the account. The agent's identity — the folder must match |
|
|
31
|
+
| `description` | what it does *and when to use it*. Other agents read this when consulting; people read it in the dashboard |
|
|
32
|
+
|
|
33
|
+
Only `name` and the body are required. Every other field widens or narrows
|
|
34
|
+
what the agent can reach, and the default is narrow.
|
|
35
|
+
|
|
36
|
+
## Which model, and how hard it thinks
|
|
37
|
+
|
|
38
|
+
| field | values | |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `model` | `fast` \| `default` \| `max`, an alias (`opus`), or a full id | a **tier**, so agents don't rot when models are renamed. Synonyms land on their tier: `small`/`cheap` → fast, `large`/`best` → max |
|
|
41
|
+
| `effort` | `low` \| `medium` \| `high` \| `xhigh` \| `max` | how long it thinks before answering. Unset leaves the model's own default |
|
|
42
|
+
| `budget` | a number in USD | the most this agent may spend in **one run**, across every step it takes in it. Its step stops mid-turn at what is left; a later step of its own in the same run is refused before it starts. Unset is no limit. Per run only — a cap over a day, week or month belongs in `AGENTS.md` |
|
|
43
|
+
|
|
44
|
+
The two are orthogonal — the model is which brain, effort is how long it
|
|
45
|
+
thinks — and `fast` + `max` is a real pairing: the cheap model, told to take
|
|
46
|
+
its time. The same word means different things on each key: `model: max` is
|
|
47
|
+
the most capable model, `effort: max` is think-hardest.
|
|
48
|
+
|
|
49
|
+
Match the tier to the work, not to the importance of the desk. A step that
|
|
50
|
+
orders findings other steps produced is a `fast` step even when its output is
|
|
51
|
+
the point of the week; a step that weighs two arguments and picks one is not.
|
|
52
|
+
|
|
53
|
+
## What it may do
|
|
54
|
+
|
|
55
|
+
| field | |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `tools` | the one grant list — built-in groups, exact SDK names, and your own tools |
|
|
58
|
+
| `disallowedTools` | subtract from what it would otherwise have |
|
|
59
|
+
| `skills` | an allowlist. **Absent inherits every skill in scope**; `[]` withholds all of them |
|
|
60
|
+
| `agents` | colleagues it may consult mid-run — an answer, no tools |
|
|
61
|
+
| `subagents` | colleagues it may delegate a whole job to — own context, own tools, never more than its own ([below](#delegating-to-a-sub-agent)) |
|
|
62
|
+
| `scripts` | programs in `scripts/`, each becoming a callable tool |
|
|
63
|
+
| `apis` | an HTTP API declared inline, as one tool |
|
|
64
|
+
| `mcpServers` | an MCP server declared inline |
|
|
65
|
+
| `secrets` | vault entries its tools may use — names only, never values |
|
|
66
|
+
| `limits` | the most calls one step may make — per tool, per web action, or in all: `limits: {web.search: 40, crm: 20, calls: 300}`. A call past a limit is refused, never run ([Tools](tools#limits--how-many-calls-a-step-may-make)) |
|
|
67
|
+
| `permissionMode` | `plan` makes the run read-only, whatever else was granted |
|
|
68
|
+
| `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 |
|
|
69
|
+
|
|
70
|
+
The built-ins, by what they reach. Only **Web** talks to an outside vendor, so
|
|
71
|
+
only Web takes a provider:
|
|
72
|
+
|
|
73
|
+
| category | grant | gives | provider |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| **Web** | `web` | eight actions — search, fetch, browse, crawl, map, extract, answer, monitor | one per action under `web:` — unset is ours |
|
|
76
|
+
| **Files** | `read` | Read, Glob, Grep — inspect but never modify | — |
|
|
77
|
+
| | `write` | Read, Write, Edit, Glob, Grep | — |
|
|
78
|
+
| **Code** | `code` | Bash — runs anything in the sandbox: Python and Node (packages via `runtime:`), or a binary you ship | — |
|
|
79
|
+
| **Memory** | `search` | `search_files(query)` over knowledge, memory, state and storage at every scope | — |
|
|
80
|
+
| | `history` | `recall_runs()` and `read_run(id)` — the workspace's last thirty finished runs | — |
|
|
81
|
+
| | `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 | — |
|
|
82
|
+
| **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)) | — |
|
|
83
|
+
| **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)) | — |
|
|
84
|
+
|
|
85
|
+
### Asking a person mid-step
|
|
86
|
+
|
|
87
|
+
`tools: [ask]` gives the agent `ask_person`. It posts a question — with up to
|
|
88
|
+
ten `options` when the answer is one of a few — and waits. The question shows
|
|
89
|
+
on the run page, in the dashboard's banner, in `foldrun approvals`, and goes
|
|
90
|
+
wherever the workspace's `notify:` sends approvals. A person answers with a
|
|
91
|
+
click, a sentence, `foldrun answer <run-id> "…"` or `POST
|
|
92
|
+
…/runs/<id>/answer`; the agent gets the answer within a second or two and
|
|
93
|
+
carries on in the same step. Nobody answering in time is an answer too: the
|
|
94
|
+
agent is told to decide if that is safe, or stop and reply BLOCKED.
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
tools: [read, write, ask]
|
|
98
|
+
ask:
|
|
99
|
+
timeout: 2h # how long it may wait — default 30m, at most 24h
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**It costs while it waits.** The step's sandbox stays up, so the wait is
|
|
103
|
+
charged as the step's seconds like any other. An approval gate (`!`) parks
|
|
104
|
+
the run and costs nothing while parked — use `ask` for a question that only
|
|
105
|
+
comes up halfway through, a gate for one you know about before the step.
|
|
106
|
+
|
|
107
|
+
**Messages into a running step.** On the platform, a person can also say
|
|
108
|
+
something to any running agent — the run page's box, `foldrun message
|
|
109
|
+
<run-id> "…"`, or `POST …/runs/<id>/message`. It reaches the model after its
|
|
110
|
+
next tool call, as context; it never changes the step's tools or what it may
|
|
111
|
+
do. A model writing without calling tools hears it at the next call it makes.
|
|
112
|
+
Every question, answer and delivered message is on the step's trace and its
|
|
113
|
+
record. A local `foldrun run` has no inbox; there `ask_person` asks on the
|
|
114
|
+
terminal when there is one.
|
|
115
|
+
|
|
116
|
+
**Retired**, still granted so a deployed agent keeps running, and each an
|
|
117
|
+
error in `foldrun check` naming what to write instead: `files` and `bash`
|
|
118
|
+
(now `write` and `code`). The web is `web` alone; Anthropic's `WebSearch` and
|
|
119
|
+
`WebFetch` are not names to grant — a model provider's own search is chosen
|
|
120
|
+
under `web:` (`web: {search: zai}`), and anything else is an unknown tool.
|
|
121
|
+
|
|
122
|
+
Anything in `tools:` that no built-in claims is one of your own tools,
|
|
123
|
+
resolved against the workspace's `tools/` and then the account library. A
|
|
124
|
+
`[[link]]` makes that explicit and is the only way to grant a tool whose name
|
|
125
|
+
a built-in would otherwise shadow:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
tools:
|
|
129
|
+
- read # the built-in group
|
|
130
|
+
- [[search]] # your tools/search.md, not the platform's search group
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`{bash: ask}` is accepted for per-call approval in Claude Code; platform runs
|
|
134
|
+
disable ask-mode and log that they did.
|
|
135
|
+
|
|
136
|
+
## Consulting colleagues
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
agents:
|
|
140
|
+
- [[fact-checker]]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Each name becomes a `consult_<name>(question)` tool. The colleague's persona
|
|
144
|
+
answers one self-contained question as a **toolless** call, inline, with the
|
|
145
|
+
spend landing on the consulting step. Depth is one — consultants cannot
|
|
146
|
+
consult further. It is deliberately weak: a consult asks a specialist what
|
|
147
|
+
they think; it does not hand over the task.
|
|
148
|
+
|
|
149
|
+
## Delegating to a sub-agent
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
subagents:
|
|
153
|
+
- [[researcher]]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
A sub-agent takes a whole job — read these forty pages, check these links,
|
|
157
|
+
work through this folder — in **its own context, with its own tools**, and
|
|
158
|
+
hands back what it found. The delegating agent's context stays on its own
|
|
159
|
+
work. It is the Agent SDK's own sub-agent (the model gets an `Agent` tool,
|
|
160
|
+
the way Claude Code delegates), fed from the named agents' files.
|
|
161
|
+
|
|
162
|
+
| | `agents:` — consult | `subagents:` — delegate |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| context | its own | its own |
|
|
165
|
+
| tools | none | its own `tools:`, cut to the delegating agent's |
|
|
166
|
+
| gives back | one answer to one question | the result of a job, in its reply |
|
|
167
|
+
| cost | on the consulting step | on the delegating step |
|
|
168
|
+
|
|
169
|
+
What keeps it inside the step:
|
|
170
|
+
|
|
171
|
+
- **Never more than its parent.** A sub-agent's tools are its own `tools:`,
|
|
172
|
+
resolved as its own step's would be, then **cut to what the delegating
|
|
173
|
+
agent holds** — a sub-agent with `tools: [read, code]` under a parent with
|
|
174
|
+
`tools: [read]` gets `read` only. Its `disallowedTools:` apply too.
|
|
175
|
+
- **Same sandbox.** Same files, same secrets and leases, the same path
|
|
176
|
+
confinement and the same approval gates: the SDK runs a sub-agent's tool
|
|
177
|
+
calls through the step's own checks, and foldrun also refuses anything
|
|
178
|
+
outside the sub-agent's list there.
|
|
179
|
+
- **One level deep.** A sub-agent never gets the `Agent` tool, so it cannot
|
|
180
|
+
delegate again.
|
|
181
|
+
- **Its `description:` is required.** The model picks a sub-agent by it; a
|
|
182
|
+
sub-agent without one is not delegated to, and `foldrun check` says so.
|
|
183
|
+
- **Its `model:`** is its own (remapped by the step's provider like the
|
|
184
|
+
step's); absent, it uses the step's model.
|
|
185
|
+
- **One step.** Flows still decide what runs next — a sub-agent is a helper
|
|
186
|
+
inside the step that called it, never a step of its own. Its spend lands on
|
|
187
|
+
that step; the run trace shows its tool calls marked with its name, and a
|
|
188
|
+
`delegating to <name>` line for each job it was given.
|
|
189
|
+
|
|
190
|
+
It works where the step works: `foldrun run` on your machine and a run on the
|
|
191
|
+
platform alike. `foldrun check` errors on a name that is not an agent and on a
|
|
192
|
+
sub-agent with no description, and warns when a sub-agent would hold none of
|
|
193
|
+
its tools inside the delegating agent's.
|
|
194
|
+
|
|
195
|
+
## Where it runs
|
|
196
|
+
|
|
197
|
+
| field | |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `size` | `small` \| `large` (default) \| `heavy` — the sandbox reservation |
|
|
200
|
+
| `runtime` | language runtimes and packages the agent's own scripts need |
|
|
201
|
+
|
|
202
|
+
`size` is a price, not just a limit. Memory is a **hard ceiling** because it is
|
|
203
|
+
not compressible: a step over it is killed, which on a single-node host
|
|
204
|
+
protects every other tenant. CPU is not capped — a step bursts to whatever is
|
|
205
|
+
free and bills at its class rate. Reference figures: 1Gi, 2Gi, 8Gi. Pick
|
|
206
|
+
`small` for an agent that mostly waits on an API, `heavy` for one that renders
|
|
207
|
+
a browser or holds real data.
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
runtime:
|
|
211
|
+
python: "3.12" # optional pin — fetched when the image lacks it
|
|
212
|
+
packages: [pandas] # pip, installed with uv
|
|
213
|
+
node: true
|
|
214
|
+
npm: [cheerio]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
An agent's own runtime and the runtimes of the tools it grants merge into one
|
|
218
|
+
environment per step, so a tool carries its own dependencies and the agent
|
|
219
|
+
does not repeat them.
|
|
220
|
+
|
|
221
|
+
The image a step runs in is small on purpose: an interpreter, `uv`, `npm`,
|
|
222
|
+
and none of the world's packages. What a step needs arrives through
|
|
223
|
+
`runtime:` — built once per distinct declaration, kept in the account's
|
|
224
|
+
cache, and reused by every later step and every agent that declares the same
|
|
225
|
+
thing. The environment comes first on `PATH`, so a tool file that says
|
|
226
|
+
`interpreter: python3`, a `#!/usr/bin/env python3` shebang and a bare
|
|
227
|
+
`python3` in Bash all find the packages. A cached environment is checked
|
|
228
|
+
before it is used, and rebuilt when it no longer holds what it promised (an
|
|
229
|
+
interrupted build, an image whose python changed). One nobody has used for
|
|
230
|
+
30 days is pruned.
|
|
231
|
+
|
|
232
|
+
Steps that need the same environment at once build it once: the others
|
|
233
|
+
wait. A build that fails leaves a `.failed` marker with its error for 60
|
|
234
|
+
seconds, and a step waiting on it stops at once with that error instead of
|
|
235
|
+
waiting out the clock. `python: false` and `node: false` mean the step wants
|
|
236
|
+
none — no environment is built for them.
|
|
237
|
+
|
|
238
|
+
On a platform, a deploy builds the environments its agents need before any
|
|
239
|
+
step does: `foldrun deploy` waits for them and prints each one, ready or
|
|
240
|
+
failed with the installer's own error, and `foldrun runtimes` asks again
|
|
241
|
+
later. A package that will not install is heard at deploy, not from the
|
|
242
|
+
first scheduled run.
|
|
243
|
+
|
|
244
|
+
## Its own model credential
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
provider:
|
|
248
|
+
name: groq
|
|
249
|
+
token: ${GROQ_API_KEY}
|
|
250
|
+
models: { fast: llama-3.3-70b-versatile }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
A name resolves to the endpoint, its wire format and where the key goes;
|
|
254
|
+
`base_url:`, `format:` (`anthropic` | `openai` | `responses`) and `auth:` (`bearer` |
|
|
255
|
+
`x-api-key`) spell the same thing out for an endpoint no preset covers.
|
|
256
|
+
`params:` passes anything else the provider accepts — `temperature`, `seed`,
|
|
257
|
+
`response_format` — merged into the request verbatim, with a `null` value
|
|
258
|
+
*removing* a field rather than setting it. See [Providers](providers).
|
|
259
|
+
|
|
260
|
+
## Running on a clock
|
|
261
|
+
|
|
262
|
+
An agent has no clock of its own — only a **flow** runs on a schedule. Put
|
|
263
|
+
`schedule:` (5-field cron, or `@hourly`/`@daily`/`@weekly`/`@monthly`) on the
|
|
264
|
+
flow that runs this agent; a single-agent job is a one-step flow. A `schedule:`
|
|
265
|
+
written in an `agent.md` fires nothing, so `check` and `deploy` refuse it
|
|
266
|
+
rather than let it deploy dead. See [Writing a flow](flows).
|
|
267
|
+
|
|
268
|
+
### `timezone:` — the calendar this agent works to
|
|
269
|
+
|
|
270
|
+
`timezone:` is also what "today" means to the agent while it works: its `TZ`
|
|
271
|
+
and the date it is told in its prompt, the same date its scripts and its
|
|
272
|
+
`verify:` shell see as `$FOLDRUN_DATE`.
|
|
273
|
+
|
|
274
|
+
It resolves nearest-wins, and each level inherits the one above it unless it
|
|
275
|
+
says otherwise:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
agent frontmatter → the flow's timezone: → workspace AGENTS.md →
|
|
279
|
+
account AGENTS.md → FOLDRUN_TIMEZONE → UTC
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
So an agent that must file against a US business day says so on its own file
|
|
283
|
+
even when its flow runs on Sydney time. Every step's trace names the zone it
|
|
284
|
+
worked to and the level that set it.
|
|
285
|
+
|
|
286
|
+
Either an IANA name (`Australia/Sydney`) or a plain fixed offset —
|
|
287
|
+
`UTC+10`, `GMT-5`, `+10:00`, `-05:30`, or a bare `UTC` — is accepted at
|
|
288
|
+
every level:
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
timezone: UTC+10
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
A value that is neither is refused by `foldrun check` and by a deploy. If one
|
|
295
|
+
reaches a run anyway it never fails the step: the step falls through to the
|
|
296
|
+
next level and says, once, what it could not read and what it used instead.
|
|
297
|
+
|
|
298
|
+
## Everything beside the file
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
agents/reporter/
|
|
302
|
+
├── agent.md # required
|
|
303
|
+
├── skills/ # one capability per folder
|
|
304
|
+
├── knowledge/ # given to it; it may read, never write
|
|
305
|
+
├── memory/ # what it learned; it writes here
|
|
306
|
+
└── scripts/ # code it can call as a tool
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`memory/` writes are the only writes an agent may make outside `outputs/`
|
|
310
|
+
without an explicit `write` grant. `knowledge/` is denied outright — through
|
|
311
|
+
the file tools and through bash.
|
|
312
|
+
|
|
313
|
+
### Where files live, from the agent's point of view
|
|
314
|
+
|
|
315
|
+
An agent's working directory is its own folder, `agents/<name>/`, and every
|
|
316
|
+
path is relative to it. The workspace root is `workspace/` — write that,
|
|
317
|
+
everywhere: in the agent's prose, in a flow's options, in a script's
|
|
318
|
+
arguments and in a shell command.
|
|
319
|
+
|
|
320
|
+
| place | write it as |
|
|
321
|
+
|---|---|
|
|
322
|
+
| a deliverable for a person | `workspace/storage/report.md` |
|
|
323
|
+
| what the next run needs | `workspace/state/history.md` |
|
|
324
|
+
| this step's scratch | `outputs/draft.md` |
|
|
325
|
+
| the account library (read-only) | `account/knowledge/prices.md` |
|
|
326
|
+
|
|
327
|
+
`workspace/` resolves the same in the file tools (Read, Write, Edit, Glob,
|
|
328
|
+
Grep), in Bash and the scripts it runs, in `scripts:` and folder tools, and
|
|
329
|
+
in a shell `verify:` — for the length of a step the agent's folder holds a
|
|
330
|
+
`workspace` link to the workspace root, removed when the step ends. The older
|
|
331
|
+
`../../storage/…` reaches the same place and keeps working; `foldrun check`
|
|
332
|
+
points it out as a hint, never an error.
|
|
333
|
+
|
|
334
|
+
A program started through the link is its own main module, as it would be
|
|
335
|
+
anywhere else. `node workspace/tools/check/run.mjs` loads the file by its
|
|
336
|
+
real path while `process.argv[1]` holds the path as typed, so the usual
|
|
337
|
+
`import.meta.url === pathToFileURL(process.argv[1]).href` check was false
|
|
338
|
+
and the program exited 0 having done nothing — a `verify:` that ran it
|
|
339
|
+
passed on every input. Every child a step starts (Bash, a shell `verify:`,
|
|
340
|
+
`scripts:` and folder tools, the Test button) now has `process.argv[1]`
|
|
341
|
+
set to the real path when it goes through the link, by a preload in
|
|
342
|
+
`NODE_OPTIONS`; nothing else about module resolution changes, and a
|
|
343
|
+
`node_modules/.bin` command runs as before. Python needs nothing:
|
|
344
|
+
`python3 workspace/x.py` is `__main__` either way. The Test button stands in
|
|
345
|
+
an agent's folder with the same link, so a tool that reads
|
|
346
|
+
`workspace/state/…` tests as it runs.
|
|
347
|
+
|
|
348
|
+
There is no `/tmp` for an agent, and no absolute path is inside its
|
|
349
|
+
workspace. A directory a tool made outside the workspace — a repository it
|
|
350
|
+
cloned, say — is reached only through that tool's own read or find action.
|
|
351
|
+
A path outside the workspace is refused with a message naming the path the
|
|
352
|
+
agent probably meant; the run page and the agent's Activity page count those
|
|
353
|
+
refusals per step, because each one is a wasted turn and a prompt that names
|
|
354
|
+
the exact path brings the count to zero.
|
|
355
|
+
|
|
356
|
+
### Handing work to a later step
|
|
357
|
+
|
|
358
|
+
A step is handed the replies of every earlier group. When the reply is a
|
|
359
|
+
report a person should also read, or a list a later step must get exactly,
|
|
360
|
+
write it to `workspace/storage/` as well — a tool that takes an `out=` path
|
|
361
|
+
and tees its own output there is the reliable way, because a model asked to
|
|
362
|
+
copy a long report by hand shortens it. Reply with one line saying what you
|
|
363
|
+
concluded; the file is the record.
|
|
364
|
+
|
|
365
|
+
## AGENTS.md — context above the agent
|
|
366
|
+
|
|
367
|
+
Two scopes, both optional, both inherited: `<account>/AGENTS.md` for everyone,
|
|
368
|
+
`<workspace>/AGENTS.md` for one workspace. Frontmatter there sets defaults —
|
|
369
|
+
### `language:` — the language this agent works in
|
|
370
|
+
|
|
371
|
+
```yaml
|
|
372
|
+
language: fa-IR # a BCP-47 tag: en, en-AU, fa, pt-BR
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Prose in AGENTS.md reaches the model and nothing else. A search engine
|
|
376
|
+
asked in English answers in English whatever the agent was told; a browser
|
|
377
|
+
reports `en-US` to every page; a fetch sends no `Accept-Language`.
|
|
378
|
+
`language:` is what those read. Set it and three things happen: the prompt
|
|
379
|
+
gains one sentence naming the language, `web action=search` asks the engine in it
|
|
380
|
+
(and for its region, when the tag has one), `web action=browse` reports it as the
|
|
381
|
+
page's locale, and `web action=fetch` requests it. A `language=` argument on
|
|
382
|
+
`web action=search`, or `locale=` on `web action=browse`, overrides it for one call.
|
|
383
|
+
|
|
384
|
+
It cascades like the clock: agent frontmatter → workspace `AGENTS.md` →
|
|
385
|
+
account `AGENTS.md` → `FOLDRUN_LANGUAGE` → English. Leave it out to
|
|
386
|
+
inherit. A value that is not a tag — `Persian` where `fa` was meant — is
|
|
387
|
+
refused by `check` and by the deploy, in one sentence, rather than being
|
|
388
|
+
read as English by every tool.
|
|
389
|
+
|
|
390
|
+
### `web.browse:` — which browser, and how it presents itself
|
|
391
|
+
|
|
392
|
+
The browser's identity is part of the agent, not of each call:
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
web:
|
|
396
|
+
browse:
|
|
397
|
+
engine: firefox
|
|
398
|
+
user_agent: "Mozilla/5.0 (Macintosh; …) Chrome/153.0.0.0 Safari/537.36"
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`engine` is one of six: `chromium` (the default), `chrome`, `firefox`, `safari`, `lightpanda` or `obscura` — what each can do, and what `check` refuses to pair with it, is the table under [Engines](tools#engines) (a call names the same setting `browser=`); `version` picks a build (`stable`/`beta`/`dev` for Chrome, or a binary installed in the image); `live: true` keeps the page open between calls so a multi-step form can be driven one step per call; `user_agent`,
|
|
402
|
+
`cookies` (a vault name, never the cookies), `cookie_domain`, `storage` and
|
|
403
|
+
`storage_origin` (the same, for a login kept in localStorage or IndexedDB
|
|
404
|
+
rather than a cookie), `device`, `locale` and `timezone` complete the picture. `headless: false`
|
|
405
|
+
runs the same browser with a window (on a virtual screen where the machine
|
|
406
|
+
has none); unset is the usual no-window browser. The same key still
|
|
407
|
+
takes a vendor name on its own (`web: {browse: browserbase}`), and inside a
|
|
408
|
+
block that is `via:`. It cascades like the clock — agent, workspace, account
|
|
409
|
+
— and a call argument overrides it for one call. Full table and the reasons:
|
|
410
|
+
[Tools](tools#how-this-agents-browser-presents-itself).
|
|
411
|
+
|
|
412
|
+
The same block says what the agent may do with its browser, and those keys
|
|
413
|
+
are locks, not defaults: `allowed_domains:` (the only hosts it can reach — a
|
|
414
|
+
call may narrow the list, never widen it) and `deny:` (actions it may never
|
|
415
|
+
take, like `eval` or `download`; a misspelt one is refused by `check`).
|
|
416
|
+
Beside them: `boundaries: true` marks the page's words in every reply,
|
|
417
|
+
`init:` runs workspace scripts before each page's own, `extensions:` loads
|
|
418
|
+
unpacked Chrome extensions, `webgpu: true`, `ignore_https_errors: true`, and
|
|
419
|
+
`state_key:` names a secret that encrypts saved logins; `video: true`
|
|
420
|
+
records every call for the run page to play, and `live_view: false` stops
|
|
421
|
+
the page being shown live there. See
|
|
422
|
+
[What this agent's browser may do](tools#what-this-agents-browser-may-do).
|
|
423
|
+
|
|
424
|
+
What a call reads is not in the block: `mode`, `interactive`, `within`,
|
|
425
|
+
`delta`, and the numbered elements an action names as `"@e4"` are per call.
|
|
426
|
+
The numbers come from the last `mode=aria` (or annotated screenshot) in the
|
|
427
|
+
same `session` and are kept in the run's
|
|
428
|
+
`outputs/.browser/refs.json`, not in any file you write — see
|
|
429
|
+
[Numbered elements](tools#the-browser-in-full).
|
|
430
|
+
|
|
431
|
+
### `region:` — the country this agent works for
|
|
432
|
+
|
|
433
|
+
```yaml
|
|
434
|
+
region: au # ISO 3166: au, ir, de, us
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
A language is not a country. `pt` says nothing about whether the customer
|
|
438
|
+
pays in reais or euros, `ar` nothing about whether the weekend is Friday
|
|
439
|
+
or Saturday, `en` nothing about miles. `region:` is the one key that says
|
|
440
|
+
which country, and the runtime derives the rest rather than asking for it:
|
|
441
|
+
|
|
442
|
+
| derived | from | example for `region: ir` + `language: fa` |
|
|
443
|
+
|---|---|---|
|
|
444
|
+
| currency | a table by country | `IRR` |
|
|
445
|
+
| units | metric, or imperial for the US | `metric` |
|
|
446
|
+
| calendar, and today's date in it | the locale's first calendar | `persian — today is ۲۶ شهریور ۱۴۰۵` |
|
|
447
|
+
| week start and weekend | the locale's week rules | starts Saturday; weekend Friday |
|
|
448
|
+
| date order, 12/24-hour | the locale's formats | `۲۶ شهریور ۱۴۰۵`, 24-hour |
|
|
449
|
+
| text direction | the script | right to left — `dir="rtl"` on anything rendered |
|
|
450
|
+
|
|
451
|
+
Set it and the prompt gains a `# Locale` block stating exactly those facts,
|
|
452
|
+
`web action=search` asks the engine for that country, and the sandbox carries
|
|
453
|
+
`FOLDRUN_REGION`, `FOLDRUN_CURRENCY`, `FOLDRUN_UNITS`, `FOLDRUN_CALENDAR`,
|
|
454
|
+
`FOLDRUN_DATE_LOCAL` (only when the calendar is not Gregorian) and
|
|
455
|
+
`FOLDRUN_RTL` for scripts. Three overrides exist for the cases where a
|
|
456
|
+
derivation is wrong for a business — `currency: usd` for an Iranian firm
|
|
457
|
+
that invoices in dollars, `calendar: gregory` for one that files to a
|
|
458
|
+
Gregorian regulator, `units: imperial` — and nothing else is a key.
|
|
459
|
+
|
|
460
|
+
It cascades like `language:`: agent → workspace `AGENTS.md` → account
|
|
461
|
+
`AGENTS.md` → `FOLDRUN_REGION` → the region in the language tag (`en-AU`
|
|
462
|
+
says AU) → none. With none, nothing is invented: no currency, metric,
|
|
463
|
+
Gregorian. `region: Australia`, `currency: dollars`, `units: both` and a
|
|
464
|
+
calendar the runtime does not know are refused by `check` and the deploy.
|
|
465
|
+
|
|
466
|
+
`provider:`, `runtime:`, `timezone:` (see above — the clock cascades down
|
|
467
|
+
from here), `notify:`, `budget:` (a cap in USD over
|
|
468
|
+
a month — `60` — or a day or week — `60/day`, `60/week`; unset is no limit),
|
|
469
|
+
`concurrency:` (in the account's file: how many of its runs go at once — the
|
|
470
|
+
next one waits its turn; see [Budgets and billing](budgets-and-billing)),
|
|
471
|
+
`limits:` (per-step call limits, as defaults — the one key that merges: an
|
|
472
|
+
agent's `limits: {web.search: 40}` overrides that key and keeps the rest),
|
|
473
|
+
`web.browse:` (see above),
|
|
474
|
+
`foldrun_version:` — and the body is context every agent works under.
|
|
475
|
+
|
|
476
|
+
Nearest wins, but a **key** replaces the whole value rather than merging into
|
|
477
|
+
it: an agent's `notify:` replaces the account's, it does not add to it.
|
|
478
|
+
|
|
479
|
+
## Checking it
|
|
480
|
+
|
|
481
|
+
`foldrun check` validates all of this offline, before any model is called —
|
|
482
|
+
a tool, skill or colleague that names nothing, a step that names a missing
|
|
483
|
+
agent, a broken script path. Run it the way you would run a typecheck.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Using the API
|
|
2
|
+
|
|
3
|
+
What every route has in common: how to sign a request, which version answers
|
|
4
|
+
it, how fast you may call, how to retry safely, and what an error looks like.
|
|
5
|
+
The routes themselves are in [api](api.md); the machine-readable description
|
|
6
|
+
is `GET /api/openapi.json` (OpenAPI 3.1, no key needed), and the dashboard
|
|
7
|
+
renders it at **Docs → API reference**.
|
|
8
|
+
|
|
9
|
+
## Authentication
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
curl -H "Authorization: Bearer $FOLDRUN_TOKEN" "$FOLDRUN_URL/api/workspaces"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A key comes from **Settings → API keys** and carries a role (viewer, editor,
|
|
16
|
+
admin) and a workspace scope no wider than the person who minted it. The
|
|
17
|
+
dashboard uses the session cookie instead; both go through the same funnel. A
|
|
18
|
+
handful of routes are a person's alone and refuse a key — see
|
|
19
|
+
[api](api.md#authentication).
|
|
20
|
+
|
|
21
|
+
## Versions
|
|
22
|
+
|
|
23
|
+
The API is versioned by date, Stripe-style. Send the version your code was
|
|
24
|
+
written against:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Foldrun-Version: 2026-10-01
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every response says which version answered it, in `Foldrun-Version`.
|
|
31
|
+
|
|
32
|
+
- **No header**: your account's pin (**Settings → API**, or
|
|
33
|
+
`PATCH /api/account/api {"version": "2026-10-01"}`), and with no pin, the
|
|
34
|
+
current version.
|
|
35
|
+
- **An unknown version** — a typo, a date that was never released — is a
|
|
36
|
+
`400`, not a silent fallback to current.
|
|
37
|
+
- **What moves the version**: only a change an existing caller would break
|
|
38
|
+
on — a field removed or renamed, a type changed, a status code changed. A
|
|
39
|
+
new route, a new optional field or a new value in a list does not; write
|
|
40
|
+
clients that ignore fields they do not know.
|
|
41
|
+
- **Older pins keep their shape.** When a breaking change ships, a caller
|
|
42
|
+
pinned before it gets the response transformed back to the shape its
|
|
43
|
+
version promised.
|
|
44
|
+
|
|
45
|
+
The CLI sends the version it was built for on every call.
|
|
46
|
+
|
|
47
|
+
### Deprecation policy
|
|
48
|
+
|
|
49
|
+
- A version stays supported for **at least 12 months** after the version that
|
|
50
|
+
replaces it is released. A pin to a retired version is answered as the
|
|
51
|
+
current version, never refused.
|
|
52
|
+
- Every breaking change is announced in **What's new** (the dashboard's
|
|
53
|
+
changelog, and `GET /api/changelog`) on the day it ships, with the date the
|
|
54
|
+
previous shape stops being served.
|
|
55
|
+
- A route on its way out answers with
|
|
56
|
+
[`Deprecation`](https://www.rfc-editor.org/rfc/rfc9745) (`@<unix time>` it
|
|
57
|
+
was deprecated) and, once the date is fixed,
|
|
58
|
+
[`Sunset`](https://www.rfc-editor.org/rfc/rfc8594) (the HTTP date it stops
|
|
59
|
+
answering), plus `Link: <…>; rel="deprecation"` to where the replacement is
|
|
60
|
+
described. The sunset is at least six months after the deprecation.
|
|
61
|
+
|
|
62
|
+
## Rate limits
|
|
63
|
+
|
|
64
|
+
Every authenticated request counts against two buckets, and both must have
|
|
65
|
+
room: one for **the key or person** making it, one for **the whole account**
|
|
66
|
+
(three times the first). Reads (`GET`, `HEAD`) and writes count separately,
|
|
67
|
+
per one-minute window. Health, status and version reads
|
|
68
|
+
(`/api/healthz`, `/api/status`, `/api/version`), signing out and ending a
|
|
69
|
+
support view are never counted or refused:
|
|
70
|
+
|
|
71
|
+
| Plan | Reads / key | Writes / key | Reads / account | Writes / account |
|
|
72
|
+
|---|---|---|---|---|
|
|
73
|
+
| none (trial, self-hosted) | 300 | 60 | 900 | 180 |
|
|
74
|
+
| Starter | 600 | 120 | 1,800 | 360 |
|
|
75
|
+
| Creator | 1,200 | 240 | 3,600 | 720 |
|
|
76
|
+
| Pro | 3,000 | 600 | 9,000 | 1,800 |
|
|
77
|
+
| Scale | 6,000 | 1,200 | 18,000 | 3,600 |
|
|
78
|
+
|
|
79
|
+
Every response carries the bucket closest to its limit:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
X-RateLimit-Limit: 600
|
|
83
|
+
X-RateLimit-Remaining: 597
|
|
84
|
+
X-RateLimit-Reset: 1790000000 # Unix seconds when the window resets
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Over the limit the answer is `429`, with `Retry-After` in seconds and
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{ "error": "rate limit: this API key may make 120 writes a minute — retry in 41s", "retry_after": 41 }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Your account's limits are on **Settings → API** and at `GET /api/account/api`.
|
|
94
|
+
The counters are shared by every server in the deployment. A self-hosted
|
|
95
|
+
install can switch them off with `FOLDRUN_API_RATE_LIMIT=off` (for example
|
|
96
|
+
behind its own gateway).
|
|
97
|
+
|
|
98
|
+
## Retrying safely: Idempotency-Key
|
|
99
|
+
|
|
100
|
+
The `POST`s that start or change something — run a flow, agent or eval,
|
|
101
|
+
rerun, deploy, upload an asset, create a flow, set a secret, mint a key,
|
|
102
|
+
approve, answer or message a run, billing actions — accept
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Idempotency-Key: <a UUID you generate, one per operation>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- The **same key with the same body** within 24 hours answers with the first
|
|
109
|
+
response — same status, same body — and `Idempotent-Replayed: true`. No
|
|
110
|
+
second run, no second charge.
|
|
111
|
+
- The **same key with a different body** is a `422`: a key names one request.
|
|
112
|
+
- The same key while the first request is still being answered is a `409`
|
|
113
|
+
with `Retry-After: 1`.
|
|
114
|
+
- A `5xx` is not remembered, so a retry after a server failure is a real
|
|
115
|
+
second attempt.
|
|
116
|
+
- A request that never finished (the server restarted mid-answer) holds its
|
|
117
|
+
key for at most two minutes, then the key is free for a real retry.
|
|
118
|
+
- Minting or rotating a key: a replay says what was done but its `key` is
|
|
119
|
+
`null` — the secret is shown once and never stored. Revoke that key and
|
|
120
|
+
mint another.
|
|
121
|
+
|
|
122
|
+
Keys belong to the caller — your account and the API key or person sending
|
|
123
|
+
them; another key of the same account never gets your stored answer — and to
|
|
124
|
+
the route: use a fresh UUID per
|
|
125
|
+
operation, and the same one when you retry it. The OpenAPI document marks
|
|
126
|
+
every operation that honours the header. The CLI sends one on every such
|
|
127
|
+
call and reuses it on its own retry.
|
|
128
|
+
|
|
129
|
+
## Errors
|
|
130
|
+
|
|
131
|
+
Every error is JSON with one field you can show a person:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "error": "flow nightly not found" }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Status | Means |
|
|
138
|
+
|---|---|
|
|
139
|
+
| 400 | the request is malformed, or names an unknown `Foldrun-Version` |
|
|
140
|
+
| 401 | no credential, or a revoked one |
|
|
141
|
+
| 402 | out of credit |
|
|
142
|
+
| 403 | your role or scope does not cover this |
|
|
143
|
+
| 404 | no such thing |
|
|
144
|
+
| 409 | conflict — a run already live (`overlap: skip`), a file changed under `ifMatch`, an idempotent request still in flight |
|
|
145
|
+
| 413 | body over 10 MB |
|
|
146
|
+
| 422 | a refusal to read: a deploy with issues, an Idempotency-Key reused with a different body |
|
|
147
|
+
| 429 | rate limited — wait `Retry-After` |
|
|
148
|
+
| 5xx | our fault; the detail is in our logs, not the response. Safe to retry with the same Idempotency-Key |
|
|
149
|
+
|
|
150
|
+
## Pagination
|
|
151
|
+
|
|
152
|
+
There is no cursor yet. The lists that can grow — runs — answer newest first
|
|
153
|
+
and take `limit` (`/api/workspaces/<ws>/runs?limit=`, default 50, at most
|
|
154
|
+
500). Everything else (flows, agents, keys,
|
|
155
|
+
secrets, workspaces) comes whole. A cursor, when one is added, will be a new
|
|
156
|
+
optional parameter and will not move the API version.
|