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/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.