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/runs.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Runs
|
|
2
|
+
|
|
3
|
+
A run is the record of a flow being driven from its first step to its last:
|
|
4
|
+
every step's reply, every tool call, every cost, and one line at the top that
|
|
5
|
+
says what happened. It is the unit of the debug loop and the thing a person
|
|
6
|
+
reads when they were not there.
|
|
7
|
+
|
|
8
|
+
## The record
|
|
9
|
+
|
|
10
|
+
Each run holds its steps in group order. A step carries:
|
|
11
|
+
|
|
12
|
+
| field | what it is |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `status` | `pending`, `running`, `awaiting-approval`, `completed`, `failed`, `skipped` |
|
|
15
|
+
| `result` | everything the agent wrote, every turn joined |
|
|
16
|
+
| `conclusion` | the final turn alone — what it concluded |
|
|
17
|
+
| `data` | the parsed value, for a step with `output: json` |
|
|
18
|
+
| `events` | tool calls, script runs with their timing, verify verdicts, refusals |
|
|
19
|
+
| `costUsd`, `tokens`, `computeSecs`, `startupSecs` | what it cost and how long the sandbox lived — **summed across its attempts** |
|
|
20
|
+
| `image` | which runner image the step's sandbox ran (isolated runs on a cluster): `variant` `full` or `slim`, `why`, and `pod: true` when a browsing step ran slim and browsed through the account's browser pod ([slim steps and a lost pod](tools#slim-steps-and-a-lost-pod)) |
|
|
21
|
+
| `browserPod` | on a step that browsed through the pod from slim: `reconnects` (tries), `reconnected` (how many got through: the call went on after it) and `closedAgain` (how many reached the pod only for the call to drop again at once, when any did); when the pod was lost, `lost` (`cause` `lost` or `needs-full`, and `detail`) and `writes` (what the step had written by then, worded); then `fallback: "browser pod lost; re-ran on full"` when it had only read and ran again on the full image, or `failure` — `browser pod died after the step had written <what>` — when it had written and was not re-run |
|
|
22
|
+
| `tries` | one row per attempt, oldest first (and a `status: "lost"` row, `image: "slim"`, before the full re-run when the browser pod was lost under a slim step that had only read — see [tools](tools#slim-steps-and-a-lost-pod)): `n` (the try, numbered in the order they ran), `attempt` (the step's attempt it belongs to, only when that is not `n` — the full re-run after a lost slim try 1 is `n: 2, attempt: 1`), `status`, `costUsd`, `tokens`, `computeSecs`, `startedAt`, `finishedAt`, the attempt's `image` and its `browserPod` line when it had one, and the attempt's last `error` when it failed. A `retry: 2` step that failed twice and then passed has three rows, and its `costUsd` is all three — the run's spend, the flow's `budget:` and the bill read the step's figure, so the figure is the total. A retry is handed what is left of the budget after the failed attempt spent, and nothing left means no retry, said on the record |
|
|
23
|
+
| `item` | for a fan-out instance, which item this one handled |
|
|
24
|
+
|
|
25
|
+
The run's `summary` is the **first meaningful line of the last step's
|
|
26
|
+
conclusion** — heading marks and bullets stripped. It is the row in the runs
|
|
27
|
+
list, the subject of the notification, and what a later run sees through
|
|
28
|
+
`recall_runs()`. Nothing declares it; the runtime asks every agent to open its
|
|
29
|
+
reply with one sentence saying what it concluded, and reads the line it wrote.
|
|
30
|
+
An agent that opens with "I'll start by…" makes all three useless.
|
|
31
|
+
|
|
32
|
+
## The run page
|
|
33
|
+
|
|
34
|
+
Every run has a page of its own, `/dashboard/<workspace>/runs/<run>`, and
|
|
35
|
+
every list, card, alert and search result that names a run opens it; the
|
|
36
|
+
old `?run=` links still land there. It opens on the answer: how the run
|
|
37
|
+
ended — status and the agent's verdict read together, so a run that
|
|
38
|
+
completed and reported BLOCKED shows **blocked**, not green — what it was,
|
|
39
|
+
its headline, when it started, how long it took, its steps and cost. If a
|
|
40
|
+
step failed, its reason sits right under that, one click from the step
|
|
41
|
+
(`#step-3` in the address opens and shows any step). A strip of the same
|
|
42
|
+
flow's recent runs, this one ringed, says whether this was a one-off. **←**
|
|
43
|
+
and **→** move to the older and newer run, **u** back to the list.
|
|
44
|
+
|
|
45
|
+
Below that, the step list, the trace and a timeline —
|
|
46
|
+
one bar per step with its tool calls marked in milliseconds, so a slow run
|
|
47
|
+
says which step was slow. Every step shows its instruction, what it was
|
|
48
|
+
handed from earlier groups, what it returned, its cost, and how many times it
|
|
49
|
+
reached for a path it was not allowed and was refused (see [Security](security)).
|
|
50
|
+
|
|
51
|
+
The page follows the run as it moves, gates included, without a reload: a
|
|
52
|
+
**live** dot beside the status says the stream is connected (amber while it
|
|
53
|
+
reconnects), and the browser tab reads the state and the step — `● running ·
|
|
54
|
+
step 3/7` — so you can switch away and still see where it is. Only the step
|
|
55
|
+
that is happening is open; a finished step folds to one line with its
|
|
56
|
+
headline, duration, tokens and cost, and the next step opens as it starts.
|
|
57
|
+
Click a step to pin it open or shut, or use **expand all** / **collapse
|
|
58
|
+
all** above the column. A **json** link opens the run record exactly as the
|
|
59
|
+
runner wrote it.
|
|
60
|
+
|
|
61
|
+
A step's trace marks each line for what it is — the agent's words, a tool
|
|
62
|
+
call (its name, then what it was called with, a click from whole), a verify
|
|
63
|
+
result as **verified** or **verify failed**, an error, or the runner's own
|
|
64
|
+
plumbing (egress, timings, resources), which is dimmed. Chips above it show
|
|
65
|
+
only one kind, errors included; **full height** shows all of it at once. A
|
|
66
|
+
step that drove a browser shows it live, then plays its recordings — see
|
|
67
|
+
[Tools](tools#watching-it-live-and-the-recording).
|
|
68
|
+
|
|
69
|
+
The account's home opens on **mission control**: what is **running now**
|
|
70
|
+
(each run's step and agent, and anything running over an hour flagged), what
|
|
71
|
+
**needs attention** (the newest failed, BAD or BLOCKED run of each flow in
|
|
72
|
+
the last day with why, and anything waiting for an approval), and every
|
|
73
|
+
desk's **last 24 runs** as a strip of cells beside its success rate, runs
|
|
74
|
+
today and a week's spend. Every row and every cell opens its run.
|
|
75
|
+
|
|
76
|
+
Each agent also has an **Activity** page: every step it has ever run, across
|
|
77
|
+
flows and direct runs, with that step's own headline. It is where "how has
|
|
78
|
+
this agent been going" is answered without opening runs one at a time.
|
|
79
|
+
|
|
80
|
+
## Gates
|
|
81
|
+
|
|
82
|
+
A step marked `2!` or carrying `approve: true` / `ask: <question>` parks the
|
|
83
|
+
run until a person decides. Three doors open it, and the trace says which:
|
|
84
|
+
|
|
85
|
+
- **the dashboard** — Approve or Reject on the run page, with a note that is
|
|
86
|
+
kept on the record;
|
|
87
|
+
- **the emailed link** — the notification carries approve and reject links
|
|
88
|
+
when the install has a public URL;
|
|
89
|
+
- **an external event** — a `wait: event` step is released by a POST to the
|
|
90
|
+
run's event URL, and what was posted reaches the next step.
|
|
91
|
+
|
|
92
|
+
The answer typed at an `ask:` gate reaches the next step's prompt, so "GO,
|
|
93
|
+
but skip the third one" is an instruction the publisher reads.
|
|
94
|
+
|
|
95
|
+
**Who may answer.** A flow's `approvers:` names the addresses that may decide
|
|
96
|
+
its gates, or `admins` for anyone at admin or above; unset, anyone who can
|
|
97
|
+
run the workspace can answer. On top of that, and whatever the list says,
|
|
98
|
+
**nobody approves a run they started themselves** — a gate exists so a second
|
|
99
|
+
person looks. Rejecting your own run is always allowed. An API key carries a
|
|
100
|
+
role but no address, so a key is not caught by the self-approval rule and
|
|
101
|
+
only an `approvers:` list constrains it.
|
|
102
|
+
|
|
103
|
+
**How long it waits.** By default, forever — which also means the run holds
|
|
104
|
+
its place in the account's concurrency for as long as nobody looks. A flow's
|
|
105
|
+
`approve_within:` stamps a deadline when the gate opens; past it the gate is
|
|
106
|
+
**rejected**, the run fails, and the destination hears why. Rejection is the
|
|
107
|
+
only safe way for a clock to answer a question a person was asked.
|
|
108
|
+
|
|
109
|
+
## Test runs
|
|
110
|
+
|
|
111
|
+
A test run is a real run of the flow that changes nothing outside it. Every
|
|
112
|
+
step runs — the same agents, the same files, the same read APIs, the same
|
|
113
|
+
gates — and the platform, not the desk's own script, makes sure nothing goes
|
|
114
|
+
out and nothing real is written. It exists because every desk that sends had
|
|
115
|
+
grown its own test switch inside its own code, and a switch a script can
|
|
116
|
+
reach is a switch a script can move: on one occasion a send step edited its
|
|
117
|
+
own gate file to get past its guard. The guard now lives where the step
|
|
118
|
+
cannot reach it.
|
|
119
|
+
|
|
120
|
+
Start one with the **Test run** checkbox on the Run button, `foldrun run
|
|
121
|
+
<flow> --test` locally, `foldrun invoke <flow> --test` on a platform, or
|
|
122
|
+
`POST .../flows/<flow>/run` with `{ "test": true }`. An eval's runs are test
|
|
123
|
+
runs, and so is every run in a preview workspace (`<ws>-preview-<branch>`),
|
|
124
|
+
unless the eval or the flow says `live: true` in its frontmatter — the safe
|
|
125
|
+
default is opted out of, never into. A re-run of a test run, or a run started
|
|
126
|
+
from one of its steps, stays a test.
|
|
127
|
+
|
|
128
|
+
What the platform does on a test run, in the order a step meets it:
|
|
129
|
+
|
|
130
|
+
| where | what |
|
|
131
|
+
|---|---|
|
|
132
|
+
| the egress proxy | any `POST`, `PUT`, `PATCH` or `DELETE` to a host that is not a model provider or a read-only service is **refused** with `403` and a JSON body `{ "test_mode": true, "would_have": { method, host, path, to, body } }`. A Resend `POST /emails` is instead **redirected**: the recipients become `delivered@resend.dev` (Resend's sink), the subject is prefixed `[TEST] `, and the call goes through — so a desk still gets a real message id. Reads (`GET`, `HEAD`, `OPTIONS`) pass. A host with no granted secret is refused as always |
|
|
133
|
+
| the sandbox | secrets whose name says they can send — `RESEND_API_KEY`, `TWILIO_*`, `GETREACH_API_KEY`, `GITHUB_TOKEN`, `MONDAY_API_TOKEN`, `OI_CRM_API_KEY`, `CLOUDFLARE_*`, `LINKEDIN_*`, `MEDIUM_COOKIES`, `TIKTOK_*` — are **withheld**: the env var reads `TEST_MODE_WITHHELD`, so a script that never checked fails at the provider with an auth error rather than sending. A step granting a script tool marked `outward: true` gets no real secret at all; one granting a tool marked `test_mode: allow` gets them all. The sandbox also has `FOLDRUN_TEST_MODE=1` and `FOLDRUN_RUN_TEST=1`, for scripts that branch on it. The http tools still go through the proxy, where the table above applies |
|
|
134
|
+
| the browser | the step is handed no account [browser pod](tools#where-the-browser-runs): the tool launches Chromium inside the run pod, which the run namespace's policy denies the internet, so a `web action=browse` call fails closed rather than fetching the world through a pod the policy does not select |
|
|
135
|
+
| the write-back | `state/` and `storage/` go into the sandbox as they are; what the step wrote there comes out under `runs/<id>/test-writes/state/…` and `…/storage/…`, and the workspace's copies are untouched. Nothing is harvested to the file store and nothing under `storage/public/` is published |
|
|
136
|
+
| the record | `test: true`; the summary opens with `[test]`; a **TEST** badge beside the status in the runs list and on the run page; every refusal, redirect, diversion and withheld secret is an event carrying an `effect`, and the run page's **Test run** panel lists them — "would have written state/sends.md, +3 lines" |
|
|
137
|
+
| the rest | gates still park, and the approve page says it is a test run; notifications go out only for `failed`; a `trigger: flow` chained on a test run starts as a test run |
|
|
138
|
+
|
|
139
|
+
Locally (`foldrun run --test`) there is no egress proxy, so the first row does
|
|
140
|
+
not apply: a script that ignores `FOLDRUN_TEST_MODE` and has its own way to a
|
|
141
|
+
provider is stopped only by the withheld secret. The other rows hold.
|
|
142
|
+
|
|
143
|
+
The list of hosts an outward write may reach, the secret-name patterns and
|
|
144
|
+
the Resend rewrite live in one table in core (`src/test-mode.ts`), with unit
|
|
145
|
+
tests, so "what does a test run refuse" is a file and not a search.
|
|
146
|
+
|
|
147
|
+
## Stopping, re-running, promoting
|
|
148
|
+
|
|
149
|
+
**Stop** ends the run and destroys its sandbox, not just the record. A
|
|
150
|
+
stopped run is recorded as stopped; it never quietly completes.
|
|
151
|
+
|
|
152
|
+
**Stop and re-run in bulk.** `POST /api/workspaces/<ws>/runs/bulk` applies
|
|
153
|
+
one of those two actions to every run a small filter names — statuses, a
|
|
154
|
+
flow, a time window, or an explicit list of ids. It is the tool for the
|
|
155
|
+
morning when one broken thing failed seventy runs. A call naming no filter is
|
|
156
|
+
refused rather than treated as everything, `dryRun: true` shows what would be
|
|
157
|
+
touched, and each run is its own outcome so one that cannot be stopped does
|
|
158
|
+
not abort the rest.
|
|
159
|
+
|
|
160
|
+
**Re-run from a group** (`from: N`) starts a new run at group N with the
|
|
161
|
+
earlier groups recorded as skipped, not invented — the step reads what earlier
|
|
162
|
+
steps wrote to `storage/` and `state/`, which is the durable handoff. Use it
|
|
163
|
+
to redo the interesting half after a fix.
|
|
164
|
+
|
|
165
|
+
**Promote** turns a run that worked into an eval case: its task and the
|
|
166
|
+
result it should reproduce. The eval runs on every push from then on.
|
|
167
|
+
|
|
168
|
+
## Why nothing happened
|
|
169
|
+
|
|
170
|
+
The hardest question this platform gets is not about a run — it is about a
|
|
171
|
+
run that does not exist. Every honest answer to it is a thing that
|
|
172
|
+
deliberately did not happen: a duplicate delivery, a burst still being
|
|
173
|
+
waited out, a throttled trigger, a flow switched off after a bad week, an
|
|
174
|
+
`overlap: skip`, a fire missed while nothing was running.
|
|
175
|
+
|
|
176
|
+
The Triggers page on each workspace records all of it: one row per flow
|
|
177
|
+
saying how often its trigger fired, how often that became a run, and what
|
|
178
|
+
the difference was, with the key in the flow file that decided each one. Its
|
|
179
|
+
last section is the delivery log, which answers the separate and earlier
|
|
180
|
+
question of whether a request arrived at all and was authentic — a bad token
|
|
181
|
+
never reaches a flow rule, so it appears there and nowhere else.
|
|
182
|
+
|
|
183
|
+
`GET /api/workspaces/<ws>/triggers` is the same thing as JSON.
|
|
184
|
+
|
|
185
|
+
## Handoffs
|
|
186
|
+
|
|
187
|
+
A step is handed the replies of **every earlier group**, oldest first, capped
|
|
188
|
+
at about 30,000 characters with the newest kept. For a list that a later step
|
|
189
|
+
must get exactly, or a report a person should also read, have the step write
|
|
190
|
+
it to `storage/` — a tool that takes an `out=` path and tees its own output
|
|
191
|
+
is the reliable way, because a model asked to copy a long report by hand
|
|
192
|
+
shortens it.
|
|
193
|
+
|
|
194
|
+
## What a run costs, and what stops it
|
|
195
|
+
|
|
196
|
+
Every step records what it cost. A flow's `budget:` is checked between
|
|
197
|
+
groups: the group that crosses it is the last that runs, and the run fails
|
|
198
|
+
saying so. The workspace's and the account's `budget:` over time sit above
|
|
199
|
+
that. See [Budgets and billing](budgets-and-billing).
|
|
200
|
+
|
|
201
|
+
Finished runs older than the install's retention are pruned. Their headline
|
|
202
|
+
survives in the eval history and in what agents can recall.
|
|
203
|
+
|
|
204
|
+
## The runs list
|
|
205
|
+
|
|
206
|
+
Beside the records sits `runs/.index.json`: one row per run with what a
|
|
207
|
+
list needs — id, flow, status, summary, times, and each step's agent,
|
|
208
|
+
status, cost, tokens, seconds and last error — never a reply or a trace. It
|
|
209
|
+
is written with the record and repaired from the directory by any reader
|
|
210
|
+
that finds a row missing, stale, or orphaned, so it is never trusted over
|
|
211
|
+
the files. The runs pages and the account-wide list read it; a run still
|
|
212
|
+
live is read whole; a finished run's reply and trace are opened by id.
|
|
213
|
+
|
|
214
|
+
## What survives the platform
|
|
215
|
+
|
|
216
|
+
A step is a sandbox that keeps its own log and files until the platform
|
|
217
|
+
has read them back. On a cluster, the record carries which sandbox a
|
|
218
|
+
running step is in and how much of its output has been applied — so when
|
|
219
|
+
the worker that started it goes away, a deploy rolling it or a crash, the
|
|
220
|
+
next worker **re-attaches** to the same sandbox and carries on from that
|
|
221
|
+
line. Finished steps stay finished; the interrupted one is neither re-run
|
|
222
|
+
nor lost, and the trace says `driver changed mid-step — re-attaching`. A
|
|
223
|
+
deploy no longer costs a run; a step that was quiet for an hour inside a
|
|
224
|
+
long tool call is still the same step afterwards.
|
|
225
|
+
|
|
226
|
+
Only a sandbox that has actually ended — the cluster killed it, or nobody
|
|
227
|
+
came back for it within forty minutes — is reported as such, in the
|
|
228
|
+
cluster's own words (`OOMKilled`, `Evicted`), and handed to the step's
|
|
229
|
+
`retry:` policy. Locally, with steps in containers rather than pods, an
|
|
230
|
+
interrupted step is run again from its start, as before.
|
|
231
|
+
|
|
232
|
+
A step whose program exits without reporting a result — it crashed after
|
|
233
|
+
its last tool call — no longer waits out those forty minutes. The platform
|
|
234
|
+
notices the exit within about half a minute, reads the whole log once more
|
|
235
|
+
in case only the stream missed the result, and otherwise fails the step
|
|
236
|
+
with `the step's program exited (code N) without reporting a result`
|
|
237
|
+
followed by the last lines the program printed on stderr. The sandbox is
|
|
238
|
+
still held at that point, so the files the step wrote are kept.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Scheduling and triggers
|
|
2
|
+
|
|
3
|
+
A flow starts when its trigger fires. The default is a person — a button, an
|
|
4
|
+
API call, `foldrun invoke` — and everything else is a line in the flow's
|
|
5
|
+
frontmatter.
|
|
6
|
+
|
|
7
|
+
## Triggers
|
|
8
|
+
|
|
9
|
+
| `trigger` | starts when | the first step is handed |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `manual` (default) | a person clicks Run, `foldrun invoke`, or an API call | an optional task |
|
|
12
|
+
| `schedule` | the cron matches | nothing |
|
|
13
|
+
| `once` | the instant in `at:` passes — fires once | nothing |
|
|
14
|
+
| `webhook` | an HTTP POST reaches the flow's hook URL | the request body |
|
|
15
|
+
| `email` | a message arrives at the flow's inbox URL | from, to, subject, text |
|
|
16
|
+
| `flow` | the flow in `after:` settles the way `on:` says | that run's id, status, summary, result |
|
|
17
|
+
| `storage` | a file lands under `path:` in this workspace's storage | the paths and who wrote them |
|
|
18
|
+
| `watch` | the content at `url:` changes (polled every `every:`, default 15m) | the new content |
|
|
19
|
+
|
|
20
|
+
A flow may not chain on itself. `watch` records its first sighting without
|
|
21
|
+
firing. A `once` more than six hours gone at first sighting is recorded and
|
|
22
|
+
never fired, so a flow deployed late does not run yesterday's job today.
|
|
23
|
+
|
|
24
|
+
## Schedules
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
---
|
|
28
|
+
name: rankings
|
|
29
|
+
trigger: schedule
|
|
30
|
+
schedule: "0 5 * * 3" # five-field cron
|
|
31
|
+
timezone: Australia/Sydney # IANA zone; UTC if unset
|
|
32
|
+
overlap: skip # or queue; unset, runs may overlap
|
|
33
|
+
catchup: last # or none — what a fire missed while down becomes
|
|
34
|
+
budget: 5
|
|
35
|
+
---
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`overlap:` decides what a new fire does while a run of the same flow is
|
|
39
|
+
live: `skip` consumes the occurrence — a cron re-firing over yesterday's
|
|
40
|
+
long run almost never means "run two" — and `queue` starts it and holds it
|
|
41
|
+
until the live one finishes.
|
|
42
|
+
|
|
43
|
+
`catchup:` decides what a missed fire becomes. Nothing runs while the
|
|
44
|
+
platform is down, and when it comes back the default — `catchup: last` — makes
|
|
45
|
+
**one** make-up run, however many fires were missed. `catchup: none` skips it
|
|
46
|
+
and waits for the next real occurrence, which is what a 07:00 report wants
|
|
47
|
+
when it is noticed at 11:00. A fire a minute or so late is never treated as a
|
|
48
|
+
make-up run: that is a tick running slightly behind.
|
|
49
|
+
|
|
50
|
+
`throttle:`, `debounce:`, `idempotency:` and `disable_after:` shape how often
|
|
51
|
+
a trigger becomes a run at all, and are described in
|
|
52
|
+
[the flow reference](flows.md).
|
|
53
|
+
|
|
54
|
+
**The cron trap worth knowing.** When both day-of-month and day-of-week are
|
|
55
|
+
restricted, they are OR'd, not AND'd — by this scheduler and by cron itself.
|
|
56
|
+
`0 5 1-7 * 5` looks like "the first Friday" and fires every Friday *and*
|
|
57
|
+
every day of the first week: about eleven runs a month. For a monthly job,
|
|
58
|
+
use a plain day of month (`0 5 3 * *`) and let the weekday go.
|
|
59
|
+
|
|
60
|
+
**Daylight saving.** The schedule is read in `timezone:`, and when the clocks
|
|
61
|
+
change it behaves as Vixie cron does. A job at a fixed time — neither minute
|
|
62
|
+
nor hour a `*` — fires once in an hour that happens twice, the first time
|
|
63
|
+
round, and a time the clocks skip fires once, at the first instant after the
|
|
64
|
+
gap. Sydney's `30 2 * * *` runs once on 5 April and at 03:00 on 4 October. A
|
|
65
|
+
`*` in the minute or the hour follows the clock as it is: an hourly job runs
|
|
66
|
+
in both copies of a repeated hour and not in a skipped one.
|
|
67
|
+
|
|
68
|
+
## One desk per day
|
|
69
|
+
|
|
70
|
+
A pattern that has held up: give each scheduled flow its own morning, so a
|
|
71
|
+
day's spend and a day's failure are one thing to read about. Put the flow
|
|
72
|
+
that writes to a shared repository on the day nothing else does — two flows
|
|
73
|
+
committing to the same `main` in one morning is a merge conflict at best.
|
|
74
|
+
Run the cheap measurement before the expensive action, so the action reads a
|
|
75
|
+
fresh measurement.
|
|
76
|
+
|
|
77
|
+
## Webhooks and inboxes
|
|
78
|
+
|
|
79
|
+
A `webhook` flow has a hook URL —
|
|
80
|
+
`/api/hooks/<account>/<workspace>/<flow>?token=…` — and an `email` flow an
|
|
81
|
+
inbox URL of the same shape. The token is derived, not stored; rotating it
|
|
82
|
+
(`POST …/hooks/<flow>/rotate`) stops every old URL at once. A flow with
|
|
83
|
+
`signature: github | stripe | slack | hmac` and a `signing_secret:` also has
|
|
84
|
+
the provider's signature checked, and a Slack `url_verification` body is
|
|
85
|
+
answered with its challenge and starts nothing.
|
|
86
|
+
|
|
87
|
+
## Chaining
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
trigger: flow
|
|
91
|
+
after: rankings
|
|
92
|
+
on: completed # or failed, or any
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The chained flow's first step is handed the finishing run's id, status and
|
|
96
|
+
summary — and its result, so a flow that reports on another flow reads what
|
|
97
|
+
it concluded rather than re-deriving it.
|
|
98
|
+
|
|
99
|
+
## Composition
|
|
100
|
+
|
|
101
|
+
A step may name a flow instead of an agent — `1. [[flow:weekly]] — …` — and
|
|
102
|
+
that flow's steps run in place, keeping their own parallelism. Cycles, and
|
|
103
|
+
nesting past three levels, are errors `foldrun check` reports.
|
package/docs/secrets.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Secrets
|
|
2
|
+
|
|
3
|
+
A secret is a value an agent may use and must never see written down. It is
|
|
4
|
+
stored in the platform's vault, named in an agent's `secrets:` list, and
|
|
5
|
+
handed to that agent's scripts as an environment variable at run time. It is
|
|
6
|
+
never written into the workspace, never printed into a prompt, and never in
|
|
7
|
+
git — which is what makes a workspace safe to commit.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Setting one
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
foldrun secrets set STRIPE_KEY # this workspace's; prompted, never echoed
|
|
14
|
+
foldrun secrets set STRIPE_KEY --account # the account's, shared by every workspace
|
|
15
|
+
foldrun secrets ls
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or in the dashboard under Settings → Secrets, or over the API:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
PUT /api/secrets { "name": "STRIPE_KEY", "value": "sk_live_…" }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`PUT` takes a name plus exactly one shape:
|
|
25
|
+
|
|
26
|
+
| shape | for |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `value` | a string — an API key, a password |
|
|
29
|
+
| `file` | a PEM, a certificate, a service-account JSON |
|
|
30
|
+
| `oauth2` | a connected third-party account (see below) |
|
|
31
|
+
| `service_account`, `ssh`, `api` | structured credentials the platform knows how to present |
|
|
32
|
+
|
|
33
|
+
Reads return which secrets exist, never their values.
|
|
34
|
+
|
|
35
|
+
## Scopes
|
|
36
|
+
|
|
37
|
+
A secret lives at the **account** or in one **workspace**. An agent looks in
|
|
38
|
+
its workspace first, then the account; a workspace secret shadows an account
|
|
39
|
+
one of the same name. The run trace says which scope each secret came from
|
|
40
|
+
(`secret DATAFORSEO_LOGIN ← workspace scope`), so a workspace quietly falling
|
|
41
|
+
back to a shared credential is visible rather than silent.
|
|
42
|
+
|
|
43
|
+
Put a credential at the account when every workspace should use the same one
|
|
44
|
+
— your model provider, your email sender. Put it in a workspace when it is
|
|
45
|
+
that job's — a customer's Search Console, a CRM key.
|
|
46
|
+
|
|
47
|
+
## How an agent gets it
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
---
|
|
51
|
+
name: enricher
|
|
52
|
+
tools: [crm_lookup]
|
|
53
|
+
secrets: [OI_CRM_API_KEY]
|
|
54
|
+
---
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The agent's scripts see `OI_CRM_API_KEY` in their environment and nothing
|
|
58
|
+
else. The model never sees the value: it sees that a tool exists, calls it,
|
|
59
|
+
and the script reads the variable. The assembled prompt names which secrets
|
|
60
|
+
are available and says, in as many words, never to print or write them.
|
|
61
|
+
|
|
62
|
+
A `verify:` command on a step runs with the same environment, so a check can
|
|
63
|
+
call the same API the step did.
|
|
64
|
+
|
|
65
|
+
A value with a line break in it — a PEM key, a service-account JSON — cannot
|
|
66
|
+
travel as an environment variable. It arrives as a file instead: a
|
|
67
|
+
private file in the step's copy of the workspace, with the variable holding
|
|
68
|
+
its path, and the run log names the secret that was handed over that way.
|
|
69
|
+
|
|
70
|
+
## Where the value actually is
|
|
71
|
+
|
|
72
|
+
On a cluster, the value is usually not in the sandbox at all. The worker
|
|
73
|
+
holds it and hands the step a per-step address — the **egress proxy** —
|
|
74
|
+
and every `http` tool and the model call itself send their requests through
|
|
75
|
+
it with `${NAME}` still in the header. The proxy fills the placeholder on
|
|
76
|
+
the way out, for the one host that secret was granted to, and forwards.
|
|
77
|
+
The value exists in the worker's memory and on the wire to the provider; a
|
|
78
|
+
program in the sandbox that reads its environment finds `RESEND_API_KEY`
|
|
79
|
+
set to the literal text `${RESEND_API_KEY}`. An oauth2 or service-account
|
|
80
|
+
secret is exchanged for a live token at the proxy too, so the recipe never
|
|
81
|
+
crosses either.
|
|
82
|
+
|
|
83
|
+
Script tools are the exception, because a script may need the value in
|
|
84
|
+
process — the browser seeding a site's cookies, an ssh key. A step that
|
|
85
|
+
grants such a script gets its declared secrets **materialised** into the
|
|
86
|
+
sandbox as real values, and the run trace says so:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
secrets: MEDIUM_COOKIES, RESEND_API_KEY materialised in the sandbox for browser; api tools and the model key go through the egress proxy
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A script that sends through the proxy itself declares it, and then gets the
|
|
93
|
+
placeholders like everything else:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
---
|
|
97
|
+
transport: script
|
|
98
|
+
name: crm_sync
|
|
99
|
+
run: sync.py
|
|
100
|
+
secrets: proxied # reads FOLDRUN_EGRESS, sends ${NAME} in its headers
|
|
101
|
+
---
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The proxy is an allowlist, not a door: a step may send only to the hosts
|
|
105
|
+
its declared tools and provider name, and `${STRIPE_API_KEY}` in a request
|
|
106
|
+
to api.resend.com stays literal. Every request through it is one line in
|
|
107
|
+
the trace — `egress: POST api.resend.com/emails → 200 (312ms)
|
|
108
|
+
[RESEND_API_KEY]` — and the model's own calls are grouped, `× 34`. Leases
|
|
109
|
+
live in the worker's memory, or in Redis when the install has one — as
|
|
110
|
+
ciphertext under the account's key, six-hour expiry — so a request may
|
|
111
|
+
land on any worker behind the Service and still find its lease. Without
|
|
112
|
+
a proxy configured (the CLI, a compose install without
|
|
113
|
+
`FOLDRUN_EGRESS_URL`) a step materialises everything, as it always did.
|
|
114
|
+
|
|
115
|
+
## Connected accounts
|
|
116
|
+
|
|
117
|
+
Some third parties are connected rather than pasted — Google, GitHub,
|
|
118
|
+
anything OAuth. Settings → Connections walks the consent flow once and stores
|
|
119
|
+
the result as an `oauth2` secret; the platform refreshes it. An agent names
|
|
120
|
+
it in `secrets:` like any other.
|
|
121
|
+
|
|
122
|
+
## How they are kept
|
|
123
|
+
|
|
124
|
+
Every account has its own encryption key. Secrets are sealed under it, and
|
|
125
|
+
the account keys are wrapped by one root key the platform holds. A stolen
|
|
126
|
+
copy of one account's secrets file opens nothing without the root key, and
|
|
127
|
+
the root key is not in any backup — every backup file, the platform's own
|
|
128
|
+
configuration included, is encrypted to a key the platform does not hold. See
|
|
129
|
+
[Security](security) and, for the CLI on your machine, [Running it yourself](self-hosting).
|
|
130
|
+
|
|
131
|
+
Rotating a secret is setting it again. The next run reads the new value; runs
|
|
132
|
+
in flight keep the one they started with.
|
|
133
|
+
|
|
134
|
+
## Whether a credential still works
|
|
135
|
+
|
|
136
|
+
The vault knows a secret's name, its scope and its shape. Whether the thing
|
|
137
|
+
on the other end still accepts it is a different question, and until it is
|
|
138
|
+
answered a dead key, an untouched key and a working key all render the same
|
|
139
|
+
way.
|
|
140
|
+
|
|
141
|
+
Two facts are recorded, at the places that already know them and nowhere
|
|
142
|
+
else. The egress proxy sees every http tool call and every model call,
|
|
143
|
+
because filling the credential is its job; notifications and the provider
|
|
144
|
+
check report their own. Nothing is instrumented for this.
|
|
145
|
+
|
|
146
|
+
| shown | means |
|
|
147
|
+
|---|---|
|
|
148
|
+
| accepted by *host* on *date* | the far end took it, that recently |
|
|
149
|
+
| refused by *host* on *date* | 401, 402 or 403 — the credential itself was rejected, with the date it last worked |
|
|
150
|
+
| *host* failed | a 5xx at their end, which says nothing about the key |
|
|
151
|
+
| not used yet | nothing has tried it, which is not the same as working |
|
|
152
|
+
|
|
153
|
+
Only 401, 402 and 403 count against a key. A provider failing at its own end
|
|
154
|
+
is not a reason to rotate anything, and counting it would send people to fix
|
|
155
|
+
the wrong thing.
|
|
156
|
+
|
|
157
|
+
A secret materialised into a sandbox reports nothing: the call happens inside
|
|
158
|
+
the pod, where the platform cannot see the answer. The page says so rather
|
|
159
|
+
than guessing.
|
|
160
|
+
|
|
161
|
+
Rotating or deleting a secret forgets what the old one did. A fresh key
|
|
162
|
+
showing its predecessor's refusals is the moment someone stops believing the
|
|
163
|
+
indicator.
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
The platform runs code it did not write, on behalf of people who may be
|
|
4
|
+
wrong about what it will do, next to other people's data. Everything in its
|
|
5
|
+
design follows from taking that seriously. This page is what an agent can
|
|
6
|
+
and cannot do, and why.
|
|
7
|
+
|
|
8
|
+
## Where a step runs
|
|
9
|
+
|
|
10
|
+
Every step executes in its own sandbox, created for it and destroyed after
|
|
11
|
+
it: a hardened container on a single box, a **gVisor** pod on a cluster.
|
|
12
|
+
The sandbox holds a copy of the agent's folder and nothing else — not the
|
|
13
|
+
platform's filesystem, not the vault, not another workspace. When the step
|
|
14
|
+
ends, its files copy back through a filter, and the sandbox is gone.
|
|
15
|
+
|
|
16
|
+
Startup is under a second warm, so this is not the latency it sounds like;
|
|
17
|
+
it is the boundary that lets one account's agent and another's share a
|
|
18
|
+
machine.
|
|
19
|
+
|
|
20
|
+
## What the network allows
|
|
21
|
+
|
|
22
|
+
A run pod may reach the public internet, DNS, the egress proxy where its
|
|
23
|
+
credentials are filled in, and its own account's search and browser pods —
|
|
24
|
+
and nothing else. The policy denies every private range (RFC1918) and the
|
|
25
|
+
cloud metadata endpoint — the address that, on a cloud host, hands out the
|
|
26
|
+
machine's own credentials. It cannot reach the platform's API, its
|
|
27
|
+
database, its object store, or another account's pods. Workspace files
|
|
28
|
+
come and go through presigned URLs, so no bucket credential is ever in a
|
|
29
|
+
pod. A [test run](runs#test-runs) is denied the internet and the browser
|
|
30
|
+
pod as well, at the network, so an outward call from a script fails closed
|
|
31
|
+
whatever the script does.
|
|
32
|
+
|
|
33
|
+
The CNI that enforces this is Cilium. Two things follow
|
|
34
|
+
from that for anyone writing a rule: a pod is matched by its identity —
|
|
35
|
+
its labels — never by an address range, so an `ipBlock` on the pod CIDR
|
|
36
|
+
admits nothing and every in-cluster rule is a selector; and a deny beats
|
|
37
|
+
every allow, which is how a test run keeps every ordinary allow and loses
|
|
38
|
+
the internet. Hubble records every flow and every drop, which is where a
|
|
39
|
+
"why can't this step reach X" question is answered.
|
|
40
|
+
|
|
41
|
+
What a run reaches on the internet, it reaches as itself. A tool that calls
|
|
42
|
+
a third party does so with the secret the agent was granted, and nothing
|
|
43
|
+
about the platform.
|
|
44
|
+
|
|
45
|
+
## What the file tools allow
|
|
46
|
+
|
|
47
|
+
An agent's built-in Read, Write, Edit, Glob and Grep are confined to its
|
|
48
|
+
workspace: its own folder, the workspace's shared directories, and the
|
|
49
|
+
account library read-only. A path outside — an invented `/tmp/…`, an
|
|
50
|
+
absolute path, a directory a tool cloned outside the workspace — is refused
|
|
51
|
+
with a message naming the path the agent probably meant. Three more things
|
|
52
|
+
stay off limits inside the workspace: the secrets file, the run journal,
|
|
53
|
+
and the generated index of a knowledge bundle.
|
|
54
|
+
|
|
55
|
+
Refusals are counted per step and shown on the run page. Each one is a
|
|
56
|
+
wasted turn; a prompt that names the exact path brings the count to zero.
|
|
57
|
+
|
|
58
|
+
## Secrets
|
|
59
|
+
|
|
60
|
+
An agent names a secret in `secrets:`; the model never sees the value — it
|
|
61
|
+
sees that a tool exists and calls it. Secrets are never written into a
|
|
62
|
+
workspace, never in git, and reads of the vault return names, not values.
|
|
63
|
+
|
|
64
|
+
On a cluster the value does not enter the sandbox either. The worker keeps
|
|
65
|
+
it and the step sends its API and model requests through a per-step
|
|
66
|
+
**egress proxy** with `${NAME}` placeholders; the proxy fills each one for
|
|
67
|
+
the host it was granted to and forwards. A compromised script in the pod
|
|
68
|
+
reads its environment and finds the name, not the key. The one exception
|
|
69
|
+
is a script tool that needs the value in process (a browser seeding
|
|
70
|
+
cookies), which materialises the step's secrets into the sandbox and says
|
|
71
|
+
so in the trace — see [Secrets](secrets).
|
|
72
|
+
|
|
73
|
+
At rest, each account has its own encryption key; the account keys are
|
|
74
|
+
wrapped by one root key the platform holds. A stolen copy of an account's
|
|
75
|
+
secrets opens nothing without it.
|
|
76
|
+
|
|
77
|
+
Backups are encrypted before they leave the server. The nightly archive —
|
|
78
|
+
every workspace, run record and the database — and the server's key file
|
|
79
|
+
are encrypted with age to a public key whose private half is not on the
|
|
80
|
+
server, so a copy of the backup bucket opens nothing; the job refuses to run
|
|
81
|
+
rather than ship anything unencrypted. See [Backups and restore](backups-and-restore).
|
|
82
|
+
|
|
83
|
+
Wherever a step's children run on the host itself rather than in a sandbox
|
|
84
|
+
— a script tool, a `verify:` shell, the model's own Bash tool on a laptop
|
|
85
|
+
or the host executor, a tool test in the dashboard — they start from an
|
|
86
|
+
allowlisted environment, never the platform's own: `PATH`, `HOME`, the
|
|
87
|
+
locale, the clock, proxy settings, `NODE_*`, the run's `FOLDRUN_RUN_*`
|
|
88
|
+
identifiers, and the secrets the agent declared, by name. The root key,
|
|
89
|
+
the datastore and mail credentials are not in it. A run container needs
|
|
90
|
+
none of this: its environment is the boundary, assembled from the same
|
|
91
|
+
rule before it starts.
|
|
92
|
+
|
|
93
|
+
A run's record is scrubbed the same way its events are: a secret quoted in
|
|
94
|
+
a step's reply, its conclusion or an `output: json` value is written as
|
|
95
|
+
`[redacted:NAME]`. Every hook and approval token derives from the vault's
|
|
96
|
+
own key, created the first time anything asks — never from a constant.
|
|
97
|
+
|
|
98
|
+
## Capability is structural
|
|
99
|
+
|
|
100
|
+
A step can do only what its agent's file grants: the tools it names, the
|
|
101
|
+
secrets it names, the colleagues it may consult. An agent with no tool that
|
|
102
|
+
writes cannot write, whatever it is told or tricked into. This is the
|
|
103
|
+
platform's answer to prompt injection: not a paragraph asking the model to
|
|
104
|
+
be careful, but a file in which the dangerous thing is absent.
|
|
105
|
+
|
|
106
|
+
The pattern that follows from it, and that the reference desks use: the
|
|
107
|
+
tool that *proposes* a change and the tool that *applies* it are two tools,
|
|
108
|
+
and only the step after a human gate holds the second.
|
|
109
|
+
|
|
110
|
+
## Gates and the record
|
|
111
|
+
|
|
112
|
+
A `!` on a step parks the run for a person, and the run cannot proceed
|
|
113
|
+
without a decision that is recorded — by whom, through which door, with
|
|
114
|
+
what note. An emailed approve link decides **one step, once**: its token
|
|
115
|
+
is bound to that step, expires (the flow's `approve_within:`, else a week),
|
|
116
|
+
and — when the mail went to a named approver — decides as that person, so
|
|
117
|
+
a forwarded link cannot walk past `approvers:`. Only a digest of a used
|
|
118
|
+
token is kept, and a key rotation kills every old link. Share links are 24 random
|
|
119
|
+
bytes; every failed fetch is the same 404.
|
|
120
|
+
|
|
121
|
+
Every write to a workspace is a revision with an author; every sign-in, key
|
|
122
|
+
use and role change is in the audit log; every step's tool calls, script
|
|
123
|
+
runs and refusals are in the run's events. A run can be reconstructed from
|
|
124
|
+
its record.
|
|
125
|
+
|
|
126
|
+
## People
|
|
127
|
+
|
|
128
|
+
Four roles, least privilege by route, one owner. See
|
|
129
|
+
[Team and access](team-and-access). Sessions are revocable; API keys carry a
|
|
130
|
+
role, can be given an end and rotated, show when they were last used, and
|
|
131
|
+
cannot act as a named person. The account's
|
|
132
|
+
[audit log](team-and-access#audit-log) records who did what — secrets by
|
|
133
|
+
name only — for the owner and admins to read or export. Support can view an
|
|
134
|
+
account only [read-only, for thirty minutes, with a reason the customer
|
|
135
|
+
reads](team-and-access#support-access).
|
|
136
|
+
|
|
137
|
+
## What is still yours to think about
|
|
138
|
+
|
|
139
|
+
- A model reading a web page reads whatever is on it. Give the agent the
|
|
140
|
+
narrowest tools its job needs, and put a gate before anything irreversible.
|
|
141
|
+
- A tool with write access to a third party — your repository, your CRM —
|
|
142
|
+
is a write. Scope the credential to what the tool must do.
|
|
143
|
+
- Bring-your-own-key credentials are yours; the platform stores them sealed
|
|
144
|
+
and hands them to your steps, and never sees the bill.
|
|
145
|
+
- The CLI on your machine runs steps as you, in your workspace, unless
|
|
146
|
+
`FOLDRUN_RUN_ISOLATION=container` is set. That is your laptop and your
|
|
147
|
+
call. See [Running it yourself](self-hosting).
|
|
148
|
+
|
|
149
|
+
## Reporting a vulnerability
|
|
150
|
+
|
|
151
|
+
Write to **hello@foldrun.io** with "security" in the subject. Do not open a
|
|
152
|
+
public issue. The same address is in `/.well-known/security.txt` on
|
|
153
|
+
foldrun.io and on the platform. Scope, what we will not pursue you for, and
|
|
154
|
+
the response targets are on the
|
|
155
|
+
[security page](https://foldrun.io/security/#report).
|
|
156
|
+
|
|
157
|
+
## Subprocessors and the DPA
|
|
158
|
+
|
|
159
|
+
The vendors that process customer data for the hosted platform — and the
|
|
160
|
+
integrations that only run when *you* configure them with your own key —
|
|
161
|
+
are listed at [foldrun.io/subprocessors](https://foldrun.io/subprocessors/).
|
|
162
|
+
A data processing agreement is drafted at
|
|
163
|
+
[foldrun.io/dpa](https://foldrun.io/dpa/); it is under legal review and not
|
|
164
|
+
yet offered for signature.
|
|
165
|
+
|
|
166
|
+
Whether the platform is up, and any incident, is on the status page — see
|
|
167
|
+
[Status and incidents](status).
|