foldrun 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/foldrun.mjs +17 -8
- package/docs/cli.md +32 -11
- package/docs/getting-started.md +1 -0
- package/docs/runs.md +1 -1
- package/package.json +3 -3
- package/src/commands.mjs +61 -12
package/README.md
CHANGED
package/bin/foldrun.mjs
CHANGED
|
@@ -187,7 +187,7 @@ Signing in
|
|
|
187
187
|
foldrun whoami who you are on the platform: account, role, workspaces
|
|
188
188
|
foldrun onboarding the account's getting-started steps, done or not, the next one first (--json)
|
|
189
189
|
foldrun preferences your own settings on the platform — the theme; set theme system|light|dark (a key acts for whoever minted it)
|
|
190
|
-
foldrun doctor check the path to the platform: node, CLI, account, DNS, a timed /api/healthz
|
|
190
|
+
foldrun doctor check the path to the platform: node, CLI, model credential, account, DNS, a timed /api/healthz
|
|
191
191
|
foldrun accounts every account signed in on this machine, and which one is active
|
|
192
192
|
foldrun use <name> act as one of them from here on
|
|
193
193
|
foldrun keys ls the account's API keys: created, last used, expires, scope — also create <label> [--expires 90d], rotate <id> [--grace 1h], revoke <id>
|
|
@@ -247,7 +247,7 @@ Options
|
|
|
247
247
|
--new-client connect: enter a new OAuth client even when one is saved for the secret
|
|
248
248
|
--step <n> approve, reject: decide only that step; default is every step that is waiting
|
|
249
249
|
--note "<text>" approve, reject: guidance the agent reads — or the reason for a refusal
|
|
250
|
-
--yes approve, stop, rerun (bulk), flow rotate-hook, flow draft, history restore, billing plan, repo, workspace set name, and every rm/revoke/unshare: skip the confirmation, deliberately (required with no terminal)
|
|
250
|
+
--yes approve, stop, rerun (bulk), flow rotate-hook, flow draft, history restore, restore <ws>, billing plan, repo, workspace set name, and every rm/revoke/unshare: skip the confirmation, deliberately (required with no terminal)
|
|
251
251
|
--json report: the raw run record instead of the report; observe, usage, api version, changelog, find, preferences, workspace vocabulary: the raw document; version: {cli, core, platform}
|
|
252
252
|
--events <a,b> account set notify: failed, awaiting-approval, completed
|
|
253
253
|
--failed webhooks deliveries: only the ones that gave up
|
|
@@ -274,7 +274,7 @@ Options
|
|
|
274
274
|
--grace <span> keys rotate: keep the old key working for 1h or 24h while you swap the new one in (default 0: revoked now)
|
|
275
275
|
|
|
276
276
|
Platform options (deploy, invoke, secrets, logs, keys)
|
|
277
|
-
--to <workspace> workspace on the platform (deploy default: folder name); flow add/show, agent link: edit or show the deployed copy instead of this folder
|
|
277
|
+
--to <workspace> workspace on the platform (deploy default: folder name); flow add/show, agent link: edit or show the deployed copy instead of this folder; restore <ws>: the commit, date or age to put it back to (3d, 2026-09-28T14:00)
|
|
278
278
|
--tenant <name> account to deploy into (default: default, local only)
|
|
279
279
|
--data <dir> the installation's data directory
|
|
280
280
|
--url <url> a running platform (or FOLDRUN_URL, or where you last signed in)
|
|
@@ -285,7 +285,7 @@ Platform options (deploy, invoke, secrets, logs, keys)
|
|
|
285
285
|
--quiet leave out the dim "acting as …" line a platform command prints on stderr
|
|
286
286
|
--local deploy: into the installation on this machine, even when signed in
|
|
287
287
|
--commit <sha> deploy: record which commit this is
|
|
288
|
-
--dry-run deploy: check and report, change nothing; flow add, flow set, flow trigger, flow move-step, flow paste, flow draft, history restore, agent link, agent unlink: show the diff, write nothing; stop, rerun (bulk): list what matches, touch nothing
|
|
288
|
+
--dry-run deploy: check and report, change nothing; flow add, flow set, flow trigger, flow move-step, flow paste, flow draft, history restore, agent link, agent unlink: show the diff, write nothing; restore <ws>: the diff only, restore nothing; stop, rerun (bulk): list what matches, touch nothing
|
|
289
289
|
--no-runtimes deploy: do not wait for the workspace's environments to be built
|
|
290
290
|
--force deploy: deploy even while runs are in flight; pull, storage get: overwrite local files
|
|
291
291
|
--yes deploy: allow deleting files this folder no longer has (asked otherwise; required with no terminal)
|
|
@@ -346,12 +346,21 @@ function helpFor(which) {
|
|
|
346
346
|
if (!which) return HELP;
|
|
347
347
|
const esc = which.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
348
348
|
const usage = new RegExp(`^\\s*foldrun ${esc}(\\s|$)`);
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
349
|
+
// An option's text is clauses split by ";", each "<commands>: what it
|
|
350
|
+
// does there". Only the part before a clause's first colon names
|
|
351
|
+
// commands, as a comma list — "approve, reject: …" names approve too.
|
|
352
|
+
// Matching anywhere up to a colon let prose in the text name a command:
|
|
353
|
+
// secrets' --kind reads "api (--base-url, --header "Name: value"…" and
|
|
354
|
+
// showed under `api --help`.
|
|
355
|
+
const item = new RegExp(`^${esc}(\\s|$)`);
|
|
356
|
+
const names = (text) =>
|
|
357
|
+
text.split(";").some((clause) => {
|
|
358
|
+
const colon = clause.indexOf(":");
|
|
359
|
+
return colon > 0 && clause.slice(0, colon).split(",").some((x) => item.test(x.trim()));
|
|
360
|
+
});
|
|
352
361
|
const lines = HELP.split("\n");
|
|
353
362
|
const own = lines.filter((l) => usage.test(l));
|
|
354
|
-
const opts = lines.filter((l) => /^\s*--/.test(l) &&
|
|
363
|
+
const opts = lines.filter((l) => /^\s*--/.test(l) && names(l.replace(/^\s*--\S+(\s+(\/\s+)?--\S+)*(\s"?<[^>]+>"?)?\s+/, "")));
|
|
355
364
|
if (!own.length && !opts.length) return HELP;
|
|
356
365
|
return ["", ...own, ...(opts.length ? ["", "Options:", ...opts] : []), "", "`foldrun --help` for everything."].join("\n");
|
|
357
366
|
}
|
package/docs/cli.md
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
# CLI reference
|
|
2
2
|
|
|
3
|
-
`foldrun` needs no account
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
`foldrun` needs no account, and `init` and `check` need no credential at
|
|
4
|
+
all. To run agents, set one model credential. The first one set is used:
|
|
5
|
+
|
|
6
|
+
| | |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `ANTHROPIC_API_KEY` | an API key from console.anthropic.com |
|
|
9
|
+
| `ANTHROPIC_AUTH_TOKEN` | a bearer token for a gateway, with `ANTHROPIC_BASE_URL` |
|
|
10
|
+
| `CLAUDE_CODE_OAUTH_TOKEN` | a Claude login token: run `claude setup-token` and set what it prints |
|
|
11
|
+
|
|
12
|
+
An agent that names its own `provider:` needs none of these (see
|
|
13
|
+
[Providers](providers)). Every step says which credential it ran on, in the
|
|
14
|
+
run's trail and on the run page, and `foldrun doctor` says which one this
|
|
15
|
+
machine would use. Whatever login Claude Code has on the machine is never
|
|
16
|
+
picked up by itself: with nothing set, `run` stops and says so.
|
|
17
|
+
|
|
18
|
+
Anthropic does not allow products built on its Agent SDK to offer claude.ai
|
|
19
|
+
login unless Anthropic has approved them. A login token is supported here for
|
|
20
|
+
the account that runs foldrun; check that the terms cover your use before you
|
|
21
|
+
rely on it.
|
|
22
|
+
|
|
23
|
+
An agent's own shell can read an API key, as it can when you run Claude Code
|
|
24
|
+
with one in your shell; it cannot read a login token, which Claude Code keeps
|
|
25
|
+
to itself. Script tools and `verify:` commands see neither.
|
|
26
|
+
|
|
27
|
+
An agent gets the tools its files grant and nothing of the machine it runs
|
|
28
|
+
on: a local run does not load the MCP connectors of whoever is signed in to
|
|
29
|
+
claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
9
30
|
|
|
10
31
|
## Commands
|
|
11
32
|
|
|
@@ -28,12 +49,12 @@ so the key is the way, or any other model provider named with `provider:`
|
|
|
28
49
|
| `foldrun flow show <flow>` | the flow as the canvas draws it, in a terminal: the trigger, then one block per group — parallel steps listed under their group number — each step with its chips (`approve`, `optional`, `when:`, `case:`/`else`, `each:` ×max, `loop … until`, `verify:`, `retry`, `wait`, `ask:`, `on-fail →`, `model`), and under it its agent's team: `↳ sub-agent`, `· consults`, `may ask you`. What `foldrun check` says about a step or its agent is printed on it, red for an error; exits 1 when there is one. Plain text, no colour, when the output is not a terminal. `--to <workspace>` shows the deployed copy |
|
|
29
50
|
| `foldrun flow run <flow>` | start a flow on the platform — the same command as `invoke`, spelled the way `agent run` is. `foldrun run` runs HERE; `flow run`/`invoke` run a flow on the platform; `agent run` runs one agent there |
|
|
30
51
|
| `foldrun flow rotate-hook <flow>` | a new webhook URL for the flow (`POST …/hooks/<flow>/rotate`). The old URL stops working the moment it returns, so it asks first — `--yes` with no terminal. Prints the new URL, which is itself a credential |
|
|
31
|
-
| `foldrun flow set <flow> --step <n\|agent> key=value …` | the step editor on the Flows page, from a terminal (core's `updateFlowStep`, and `updateFlowStepInstruction` for `instruction=`): `model`, `effort`, `retry`, `timeout`, `verify`, `when`, `case`, `else`, `loop`, `until`, `each`, `max`, `limits` — only the keys named are touched, `key=` clears one. `each=` takes `lines` only; the other fan-outs are `flow add … fan-out`. Shows the diff, refuses an edit `check` would call a new error, `--dry-run` writes nothing; `--to <workspace>` sends the editor's `PATCH …/flows/<flow> { step, options }` / `{ step, instruction }` with `expect` |
|
|
52
|
+
| `foldrun flow set <flow> --step <n\|agent> key=value …` | the step editor on the Flows page, from a terminal (core's `updateFlowStep`, and `updateFlowStepInstruction` for `instruction=`): `model`, `effort`, `retry`, `timeout`, `verify`, `when`, `case`, `else`, `loop`, `until`, `each`, `max`, `limits` — only the keys named are touched, `key=` clears one. `each=` takes `lines` only; the other fan-outs are `flow add … fan-out`. A value the rewriter would drop or clamp is refused, nothing written: `effort` one of low, medium, high, xhigh, max (or an alias core reads, e.g. `deep`); `timeout` a whole number of seconds (`timeout=900`, not `15m`); `retry` 0–5, `loop` 1–5, `max` 1–20, whole numbers. Shows the diff, refuses an edit `check` would call a new error, `--dry-run` writes nothing; `--to <workspace>` sends the editor's `PATCH …/flows/<flow> { step, options }` / `{ step, instruction }` with `expect` |
|
|
32
53
|
| `foldrun flow trigger <flow> manual\|webhook\|schedule` | the trigger picker on a flow card (core's `setFlowTrigger`): only the `trigger:`, `schedule:` and `timezone:` lines change. `--schedule "0 9 * * 1"` (5 fields, or `@daily`) and `--timezone <zone>` go with `schedule`. Diff, checked, `--dry-run`; `--to` sends `PATCH { trigger }` |
|
|
33
54
|
| `foldrun flow move-step <flow> --step <n\|agent>` | dragging a card on the board: `--group <g>` runs it beside group g, `--after <g>` gives it a group of its own after g (`0` = first). Only step numbers change (core's `reorderFlowSteps`). Diff, checked, `--dry-run`; `--to` sends `PATCH { groups }` |
|
|
34
55
|
| `foldrun flow copy-step <flow> --step <n\|agent>[,<n>…]` | the canvas's Copy: the steps' markdown — line, marker and every option (core's `stepSource`) — on stdout. Reads only |
|
|
35
56
|
| `foldrun flow paste <flow>` | the canvas's Paste: step markdown from stdin or `--file <path>` goes in as new groups after `--after <g>` (default last), or beside group `--group <g>`, renumbered to fit (core's `pasteSteps`) — `foldrun flow copy-step a --step 2 \| foldrun flow paste b`. Diff, checked, `--dry-run`; `--to` sends `PATCH { edit: { op: "paste" } }` |
|
|
36
|
-
| `foldrun flow draft "<what it should do>"` | **Draft with AI**, on the platform (`--to <workspace>`; `--flow <name>` redrafts an existing flow): `POST …/flows/draft` has a model write the flow and any agent files it needs, checks and repairs once, and the CLI prints the notes, every file as a diff and what the check still finds. Nothing is saved until you say yes — `--yes` on a pipe, `--dry-run` never; each file goes through `PUT …/source` with `ifMatch`, so one that changed meanwhile is refused. One model call, charged like a step |
|
|
57
|
+
| `foldrun flow draft "<what it should do>"` | **Draft with AI**, on the platform (`--to <workspace>`; `--flow <name>` redrafts an existing flow): `POST …/flows/draft` has a model write the flow and any agent files it needs, checks and repairs once, and the CLI prints the notes, every file as a diff and what the check still finds. Nothing is saved until you say yes — `--yes` on a pipe, `--dry-run` never; on a pipe without `--yes` it saves nothing and exits 1, though the draft call was made and charged; each file goes through `PUT …/source` with `ifMatch`, so one that changed meanwhile is refused. One model call, charged like a step |
|
|
37
58
|
| `foldrun tool test <name>` | exercise one tool, alone — no model, no run, no flow. The real thing runs: the HTTP request against the declared `base:`, the script with the workspace's secrets in its environment, the MCP handshake. Prints the transport, whether it exited cleanly, how long it took, everything the tool printed, and any secret it needed by NAME and never by value. Its arguments are `key=value` pairs, one per argument the tool declares, and `--path` is an http tool's probe path. It waits five minutes by default, because a tool that crawls takes as long as it takes — `--timeout <seconds>` waits longer, and a test given up on says so rather than claiming the tool failed |
|
|
38
59
|
| `foldrun run <target>` | run an agent or a flow HERE, on this machine (`name`, or `flow:name`) — `--test` for a [test run](runs#test-runs). On a platform: `flow run`/`invoke` for a flow, `agent run` for one agent |
|
|
39
60
|
| `foldrun eval [name]` | one eval, or all of them. `--to <workspace>` runs the DEPLOYED workspace's evals on the platform (`POST …/evals/<name>/run`), waiting for each verdict, and prints the same lines; exits 1 when a case fails |
|
|
@@ -78,7 +99,7 @@ so the key is the way, or any other model provider named with `provider:`
|
|
|
78
99
|
| `foldrun api version` | Settings → API: the API version this account's requests get when they send no `Foldrun-Version`, whether it is pinned, every version still served and the rate limits. `api version pin <YYYY-MM-DD>` pins it, `unpin` goes back to current (`PATCH /api/account/api`; an admin's) |
|
|
79
100
|
| `foldrun changelog` | What's new: the platform's release notes, newest first — features, fixes, docs — and which release this install runs (`GET /api/changelog`; `--limit <n>` up to 50, `--json`) |
|
|
80
101
|
| `foldrun version` | this CLI's version, its `@foldrun/core`'s, and — when a platform is known (`--url`, `--profile`, `FOLDRUN_URL` or where you signed in) — that platform's: version, release time, API version, each component's commit and the browsers its runner image has (with versions where known: `chrome 154.0.8037.58`), from `GET /api/version` (no key needed). Warns when the platform was released with a newer CLI than this one. An unreachable platform is a dim line, not a failure. `--json` prints `{ cli, core, platform }`. `foldrun --version` (or `-v`) prints only the CLI's version, offline. See [Versions and releases](versions) |
|
|
81
|
-
| `foldrun doctor` | check the road to the platform, one line each: Node (22 or newer), the CLI and core versions, which of `FOLDRUN_URL`, `FOLDRUN_TOKEN`, `FOLDRUN_TIMEOUT`, `HTTPS_PROXY` are set (never their values), the account it would act as, DNS for the platform's host, and a timed `GET /api/healthz`. The first thing to run when a command cannot reach the platform |
|
|
102
|
+
| `foldrun doctor` | check the road to the platform, one line each: Node (22 or newer), the CLI and core versions, which of `FOLDRUN_URL`, `FOLDRUN_TOKEN`, `FOLDRUN_TIMEOUT`, `HTTPS_PROXY` are set (never their values), which model credential a local run would use (by its variable, never its value), the account it would act as, DNS for the platform's host, and a timed `GET /api/healthz`. The first thing to run when a command cannot reach the platform |
|
|
82
103
|
| `foldrun storage <verb>` | what a workspace PRODUCED, as opposed to what you wrote — `source` is the other half. `ls [prefix]` lists every file with its size, when it was written, how long ago and which run wrote it (`run:<id>`, `user:<email>` for an upload, `api-key` for a key), newest first, plus what the store holds against its quota. `cat <path>` prints a text file and refuses one that is not text; `cat <path> --preview` asks the platform to read it instead and prints what is in it — a PDF or Word document as paragraphs, a spreadsheet or CSV as a table, a deck as slides, an archive as its listing. `get <path>` downloads it beside you — `--file <path>` names somewhere else, `--force` overwrites. `put <file|folder>...` uploads any files, of any type, into storage under their own names, walking folders whole (`.git`, `node_modules` and `.DS_Store` are skipped); `--into <folder/>` puts them under a folder, `--as <path>` renames a single file, and large files go straight to the bucket by a presigned URL, so size is not capped by the platform's request limit; `rm <path>` removes one. `share <path>` mints a public link to one produced file (7 days; `--ttl <days>`, `--forever`) and prints the URL; `shares` lists the live links (`--all` includes expired and revoked); `unshare <token>` revokes one — the link answers 404 from then on and the token is never reused. `--to <workspace>` from anywhere; inside a workspace folder it is already known |
|
|
83
104
|
| `foldrun runtimes` | the environments a deployed workspace's agents need — each distinct `runtime:` (an agent's own merged with its tools'), the packages in it, which agents use it, and whether the platform has built it: ready, building, or failed with the installer's own error. `foldrun deploy` prints the same thing after a deploy and waits up to two minutes for builds in progress (`--no-runtimes` skips the wait); this asks again later, and `--wait` holds on while any is building. Exits 1 when one has failed. `--to <workspace>` from anywhere |
|
|
84
105
|
| `foldrun schedule` | every flow in the account whose `trigger:` is a clock: workspace, flow, the cron line, its timezone, how many steps, whether the scheduler can parse it, and the next three times it fires. The times are the point — `0 5 1-7 * 5` reads as "the first Friday" and fires eight times in twenty-eight days, because day-of-month and day-of-week are OR'd. Exits 1 if any line is unparseable, since an invalid schedule errors nowhere and simply never runs. `--to <workspace>` narrows it |
|
|
@@ -150,7 +171,7 @@ so the key is the way, or any other model provider named with `provider:`
|
|
|
150
171
|
| `--limit <n>` | `runs`, `billing`, `history`: how many rows, newest first (`runs` default 20, `billing` 15). Each workspace is asked for its own newest `n` before the merge, so one busy desk cannot crowd out a quiet one |
|
|
151
172
|
| `--step <n>` | `approve`, `reject`: decide one gate, numbered as `report` prints it. Without it, every step of that run which is waiting |
|
|
152
173
|
| `--note "<text>"` | `approve`: guidance the approved step reads in its prompt — "approve, but skip the Sydney batch". `reject`: the reason, recorded on the trace |
|
|
153
|
-
| `--yes` | skip the confirmation, deliberately: `approve`, `stop`, `deploy` (when it would delete files), and a bulk `stop` or `rerun`, `flow rotate-hook`, `flow draft`, `history restore`, `billing plan`, `repo deploy`/`merge`, `workspace set name`, and every delete — `runs rm`, `storage rm`, `storage unshare`, `source rm`, `secrets rm`, `keys revoke`, `workspaces rm`. Required when there is no terminal to ask; without it nothing is deleted |
|
|
174
|
+
| `--yes` | skip the confirmation, deliberately: `approve`, `stop`, `deploy` (when it would delete files), and a bulk `stop` or `rerun`, `flow rotate-hook`, `flow draft`, `history restore`, `restore`, `billing plan`, `repo deploy`/`merge`, `workspace set name`, and every delete — `runs rm`, `storage rm`, `storage unshare`, `source rm`, `secrets rm`, `keys revoke`, `workspaces rm`. Required when there is no terminal to ask; without it nothing is deleted |
|
|
154
175
|
| `--json` | `report`: the raw run record instead of the report. `observe`, `usage`: the raw document |
|
|
155
176
|
| `--events <a,b>` | `account set notify`: which events are notified on — `failed`, `awaiting-approval`, `completed` |
|
|
156
177
|
| `--watch` | `invoke`: follow the run's trace here as it happens; the exit code is the run's |
|
|
@@ -175,7 +196,7 @@ so the key is the way, or any other model provider named with `provider:`
|
|
|
175
196
|
| `--timeout <s>` | how many seconds one request to the platform may take — or `FOLDRUN_TIMEOUT`. Default 30, and 300 for `tool test`, where the tool itself is the thing being waited on |
|
|
176
197
|
| `--local` | `deploy`, `secrets`, `logs`: the installation on this machine, even when signed in |
|
|
177
198
|
| `--quiet` | leave out the dim "acting as …" line a platform command prints on stderr |
|
|
178
|
-
| `--dry-run` | `extract`: list what would move, change nothing. `flow add`, `flow rm-step`, `flow dup-step`, `flow set`, `flow trigger`, `flow move-step`, `flow paste`, `flow draft`, `history restore`, `agent link`/`unlink`: show the diff, write nothing. A bulk `stop`/`rerun`: list what matches, touch nothing |
|
|
199
|
+
| `--dry-run` | `extract`: list what would move, change nothing. `flow add`, `flow rm-step`, `flow dup-step`, `flow set`, `flow trigger`, `flow move-step`, `flow paste`, `flow draft`, `history restore`, `agent link`/`unlink`: show the diff, write nothing. `restore`: the diff only, restore nothing. A bulk `stop`/`rerun`: list what matches, touch nothing |
|
|
179
200
|
| `--out <file>` | `api spec`: where to write the OpenAPI document (default stdout) |
|
|
180
201
|
| `--file <path>` | `source put`: the file to send. `storage get`, `report get`, `account export`, `billing statement --csv`: where to write — `-` for stdout |
|
|
181
202
|
|
package/docs/getting-started.md
CHANGED
|
@@ -8,6 +8,7 @@ files are the program — so the loop is edit, check, run, deploy.
|
|
|
8
8
|
```sh
|
|
9
9
|
npm install -g foldrun
|
|
10
10
|
export ANTHROPIC_API_KEY=sk-ant-... # an API key from console.anthropic.com
|
|
11
|
+
# or: export CLAUDE_CODE_OAUTH_TOKEN=... from `claude setup-token` (see the CLI reference)
|
|
11
12
|
|
|
12
13
|
foldrun init my-account
|
|
13
14
|
cd my-account
|
package/docs/runs.md
CHANGED
|
@@ -132,7 +132,7 @@ What the platform does on a test run, in the order a step meets it:
|
|
|
132
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
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
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 |
|
|
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. Each later step of the run gets those writes laid over its copy, times kept, so a flow that hands work on through `storage/` is tested as it runs live — the step's log says how many files it sees. Nothing is harvested to the file store and nothing under `storage/public/` is published |
|
|
136
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
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
138
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldrun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Agents are just folders. Write them, check them, run them — on your machine.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"node": ">=22"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@foldrun/core": "^0.
|
|
14
|
+
"@foldrun/core": "^0.6.0"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"e2e": "FOLDRUN_E2E=1 FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-e2e-XXXXXX) node --test --test-timeout=300000 tests/e2e.test.ts",
|
|
24
|
-
"test": "FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-test-XXXXXX) node --test tests/cli-account.test.ts tests/cli-approvals.test.ts tests/cli-stop.test.ts tests/cli-account-settings.test.ts tests/cli-schedule.test.ts tests/cli-billing.test.ts tests/cli-storage.test.ts tests/cli-scaffold.test.ts tests/cli-secrets.test.ts tests/cli-site-login.test.ts tests/cli-check-library.test.ts tests/cli-check-timezone.test.ts tests/cli-check-subagents.test.ts tests/cli-connect.test.ts tests/cli-accounts.test.ts tests/cli-remote.test.ts tests/cli-stream.test.ts tests/cli-doctor.test.ts tests/cli-tool-test.test.ts tests/cli-agent-run.test.ts tests/cli-check-remote.test.ts tests/cli-shares.test.ts tests/cli-triggers.test.ts tests/cli-providers.test.ts tests/cli-rerun-put.test.ts tests/cli-guide.test.ts tests/cli-runtimes.test.ts tests/cli-deploy-safety.test.ts tests/cli-operator.test.ts tests/cli-flow-patterns.test.ts tests/cli-flow-rm-step.test.ts tests/cli-flow-dup-step.test.ts tests/cli-flow-write-stale.test.ts tests/cli-agent-link-stale.test.ts tests/cli-parity.test.ts tests/cli-invoke-inputs.test.ts tests/cli-audit-keys.test.ts tests/cli-version.test.ts tests/cli-check-web-warnings.test.ts tests/cli-report-image.test.ts tests/cli-check-workspace-paths.test.ts tests/cli-parity-dashboard.test.ts tests/cli-api-standards.test.ts tests/cli-backups-onboarding.test.ts tests/cli-check-limits.test.ts tests/cli-gallery.test.ts tests/cli-status-platform.test.ts tests/cli-webhooks-notifications.test.ts",
|
|
24
|
+
"test": "FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-test-XXXXXX) node --test tests/cli-account.test.ts tests/model-credential.test.ts tests/cli-approvals.test.ts tests/cli-stop.test.ts tests/cli-account-settings.test.ts tests/cli-schedule.test.ts tests/cli-billing.test.ts tests/cli-storage.test.ts tests/cli-scaffold.test.ts tests/cli-secrets.test.ts tests/cli-site-login.test.ts tests/cli-check-library.test.ts tests/cli-check-timezone.test.ts tests/cli-check-subagents.test.ts tests/cli-connect.test.ts tests/cli-accounts.test.ts tests/cli-remote.test.ts tests/cli-stream.test.ts tests/cli-doctor.test.ts tests/cli-tool-test.test.ts tests/cli-agent-run.test.ts tests/cli-check-remote.test.ts tests/cli-shares.test.ts tests/cli-triggers.test.ts tests/cli-providers.test.ts tests/cli-rerun-put.test.ts tests/cli-guide.test.ts tests/cli-runtimes.test.ts tests/cli-deploy-safety.test.ts tests/cli-operator.test.ts tests/cli-flow-patterns.test.ts tests/cli-flow-rm-step.test.ts tests/cli-flow-dup-step.test.ts tests/cli-flow-write-stale.test.ts tests/cli-agent-link-stale.test.ts tests/cli-parity.test.ts tests/cli-invoke-inputs.test.ts tests/cli-audit-keys.test.ts tests/cli-version.test.ts tests/cli-check-web-warnings.test.ts tests/cli-report-image.test.ts tests/cli-check-workspace-paths.test.ts tests/cli-parity-dashboard.test.ts tests/cli-api-standards.test.ts tests/cli-backups-onboarding.test.ts tests/cli-check-limits.test.ts tests/cli-gallery.test.ts tests/cli-status-platform.test.ts tests/cli-webhooks-notifications.test.ts",
|
|
25
25
|
"release": "node scripts/release.mjs",
|
|
26
26
|
"typecheck": "tsc -p tsconfig.json",
|
|
27
27
|
"docs:sync": "node scripts/sync-docs.mjs",
|
package/src/commands.mjs
CHANGED
|
@@ -328,7 +328,7 @@ async function scaffoldCmd(kind, positional, flags, layout) {
|
|
|
328
328
|
|
|
329
329
|
// The other verbs are dispatched before this — `agent run` and `tool test`
|
|
330
330
|
// go to the platform — so anything left here should have been `new`.
|
|
331
|
-
const alsoVerbs = { agents: ["run", "link", "unlink"], tools: ["test"], flows: ["add", "rm-step", "dup-step", "show", "run", "rotate-hook"] }[kind] ?? [];
|
|
331
|
+
const alsoVerbs = { agents: ["run", "link", "unlink"], tools: ["test"], flows: ["add", "rm-step", "dup-step", "show", "run", "rotate-hook", "set", "trigger", "move-step", "copy-step", "paste", "draft"] }[kind] ?? [];
|
|
332
332
|
const verb = positional[0];
|
|
333
333
|
if (verb !== "new") {
|
|
334
334
|
const verbs = ["new", ...alsoVerbs].join(", ");
|
|
@@ -1124,16 +1124,22 @@ function bundleDirs(workspace, kind) {
|
|
|
1124
1124
|
|
|
1125
1125
|
// ---------------------------------------------------------------- run
|
|
1126
1126
|
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1127
|
+
// A model credential, named, and never the machine's own login by default:
|
|
1128
|
+
// an API key (ANTHROPIC_API_KEY), a gateway bearer (ANTHROPIC_AUTH_TOKEN),
|
|
1129
|
+
// or a Claude login token (CLAUDE_CODE_OAUTH_TOKEN, from `claude
|
|
1130
|
+
// setup-token`) — in that order, and the run says which (core
|
|
1131
|
+
// model-credential.ts). Before, the key was checked here and then never
|
|
1132
|
+
// reached the step, which ran on whatever login the machine had. An agent
|
|
1133
|
+
// that names its own `provider:` needs none of these — that is checked where
|
|
1134
|
+
// the agent is read. Terms: Anthropic does not allow products built on its
|
|
1135
|
+
// Agent SDK to offer claude.ai login unless it has approved them.
|
|
1136
|
+
const MODEL_CREDENTIALS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"];
|
|
1132
1137
|
function assertCredentials() {
|
|
1133
|
-
if (
|
|
1138
|
+
if (MODEL_CREDENTIALS.some((v) => (process.env[v] ?? "").trim())) return;
|
|
1134
1139
|
throw new Error(
|
|
1135
|
-
"no
|
|
1136
|
-
" or
|
|
1140
|
+
"no model credential — set ANTHROPIC_API_KEY (an API key from console.anthropic.com),\n" +
|
|
1141
|
+
" or CLAUDE_CODE_OAUTH_TOKEN (a Claude login token: `claude setup-token`),\n" +
|
|
1142
|
+
" or give the agent its own `provider:`. `foldrun check` works without any.",
|
|
1137
1143
|
);
|
|
1138
1144
|
}
|
|
1139
1145
|
|
|
@@ -3321,6 +3327,20 @@ async function doctor(flags) {
|
|
|
3321
3327
|
// Set or unset, never the value: two of these are a key and a proxy URL
|
|
3322
3328
|
// that may carry one.
|
|
3323
3329
|
line(true, "env", ["FOLDRUN_URL", "FOLDRUN_TOKEN", "FOLDRUN_TIMEOUT", "HTTPS_PROXY"].map((v) => (env(v) ? `${v} set` : c.dim(`${v} unset`))).join(" · "));
|
|
3330
|
+
// Which model credential a local run would use — said, not judged: a
|
|
3331
|
+
// machine that only drives the platform needs none.
|
|
3332
|
+
try {
|
|
3333
|
+
const { resolveModelCredential } = await core();
|
|
3334
|
+
const m = resolveModelCredential(process.env);
|
|
3335
|
+
console.log(
|
|
3336
|
+
` ${m.kind === "none" ? c.dim("·") : c.green("✓")} ${"model".padEnd(9)} ` +
|
|
3337
|
+
(m.kind === "none"
|
|
3338
|
+
? c.dim("no credential for local runs — ANTHROPIC_API_KEY, or CLAUDE_CODE_OAUTH_TOKEN from `claude setup-token`")
|
|
3339
|
+
: `${m.label} — what \`foldrun run\` uses here`),
|
|
3340
|
+
);
|
|
3341
|
+
} catch {
|
|
3342
|
+
// core unresolvable is already its own line above
|
|
3343
|
+
}
|
|
3324
3344
|
|
|
3325
3345
|
let url;
|
|
3326
3346
|
let token;
|
|
@@ -7571,6 +7591,16 @@ async function flowEditCmd(flags, layout, { flow, what, label, compute, bodies }
|
|
|
7571
7591
|
return 0;
|
|
7572
7592
|
}
|
|
7573
7593
|
|
|
7594
|
+
/** Two parsed flows that run the same: equal but for key order and the
|
|
7595
|
+
* line numbers a reflow moves. */
|
|
7596
|
+
function sameFlow(a, b) {
|
|
7597
|
+
const canon = (v) =>
|
|
7598
|
+
Array.isArray(v) ? v.map(canon)
|
|
7599
|
+
: v && typeof v === "object" ? Object.fromEntries(Object.keys(v).filter((k) => k !== "line").sort().map((k) => [k, canon(v[k])]))
|
|
7600
|
+
: v;
|
|
7601
|
+
return JSON.stringify(canon(a)) === JSON.stringify(canon(b));
|
|
7602
|
+
}
|
|
7603
|
+
|
|
7574
7604
|
/** The step options the dashboard's step editor writes (core updateFlowStep). */
|
|
7575
7605
|
const STEP_OPTION_KEYS = ["model", "effort", "retry", "timeout", "verify", "when", "case", "else", "loop", "until", "each", "max", "limits"];
|
|
7576
7606
|
|
|
@@ -7586,6 +7616,11 @@ async function flowSetCmd(positional, flags, layout) {
|
|
|
7586
7616
|
const pairs = positional.slice(2);
|
|
7587
7617
|
const usage = `\`foldrun flow set <flow> --step <n|agent> key=value …\` — keys: ${STEP_OPTION_KEYS.join(", ")}, instruction (key= clears)`;
|
|
7588
7618
|
if (!flow || !pairs.length) throw new Error(usage);
|
|
7619
|
+
const { resolveEffort, EFFORT_LEVELS, parseDuration } = await core();
|
|
7620
|
+
// The counted options, in the ranges core's rewriter and parseFlow share.
|
|
7621
|
+
// Outside them the rewriter clamps, zeroes (retry=abc wrote 0) or drops
|
|
7622
|
+
// the line (timeout=15m deleted timeout: 900) and the edit "succeeded".
|
|
7623
|
+
const COUNTS = { retry: [0, 5, "extra attempts"], loop: [1, 5, "extra cycles"], max: [1, 20, "items"] };
|
|
7589
7624
|
const options = {};
|
|
7590
7625
|
let instruction;
|
|
7591
7626
|
for (const p of pairs) {
|
|
@@ -7603,9 +7638,18 @@ async function flowSetCmd(positional, flags, layout) {
|
|
|
7603
7638
|
// dropped without a word. Refuse it here and say where it lives.
|
|
7604
7639
|
if (key === "each" && value && value !== "lines") throw new Error(`each=${value}: flow set writes \`each: lines\` only — \`foldrun flow add <flow> fan-out --each "${value}"\` writes the rest`);
|
|
7605
7640
|
if (key === "else" && value && !["true", "1"].includes(value)) throw new Error("else=true, or else= to clear it");
|
|
7641
|
+
if (key === "effort" && value && !resolveEffort(value)) throw new Error(`effort=${value} is not an effort — ${EFFORT_LEVELS.join(", ")} (or effort= to clear it)`);
|
|
7642
|
+
if (key === "timeout" && value && !/^[1-9]\d*$/.test(value)) {
|
|
7643
|
+
const secs = parseDuration(value);
|
|
7644
|
+
throw new Error(`timeout=${value}: flow set writes a whole number of seconds — ${secs ? `timeout=${secs}` : "e.g. timeout=900 for 15 minutes"} (or timeout= to clear it)`);
|
|
7645
|
+
}
|
|
7646
|
+
if (key in COUNTS && value) {
|
|
7647
|
+
const [lo, hi, what] = COUNTS[key];
|
|
7648
|
+
if (!/^\d+$/.test(value) || Number(value) < lo || Number(value) > hi) throw new Error(`${key}=${value}: a whole number of ${what}, ${lo} to ${hi} (or ${key}= to clear it)`);
|
|
7649
|
+
}
|
|
7606
7650
|
options[key] = value === "" ? null : value;
|
|
7607
7651
|
}
|
|
7608
|
-
const { updateFlowStep, updateFlowStepInstruction } = await core();
|
|
7652
|
+
const { updateFlowStep, updateFlowStepInstruction, parseFlow } = await core();
|
|
7609
7653
|
return flowEditCmd(flags, layout, {
|
|
7610
7654
|
flow,
|
|
7611
7655
|
what: "flow set",
|
|
@@ -7617,6 +7661,10 @@ async function flowSetCmd(positional, flags, layout) {
|
|
|
7617
7661
|
const afterInstruction = instruction !== undefined ? updateFlowStepInstruction(next, i, instruction) : next;
|
|
7618
7662
|
next = afterInstruction;
|
|
7619
7663
|
if (Object.keys(options).length) next = updateFlowStep(next, i, options);
|
|
7664
|
+
// The rewriter re-emits the managed lines at the top of the step, so a
|
|
7665
|
+
// value the step already has still came back reordered — a diff, and on
|
|
7666
|
+
// the platform a revision, for no change. Same flow when parsed: none.
|
|
7667
|
+
if (next !== raw && sameFlow(parseFlow("flow.md", raw), parseFlow("flow.md", next))) next = raw;
|
|
7620
7668
|
return { next, meta: { i, afterInstruction, label: `step ${i + 1} · ${s.subflow ? `flow:${s.subflow}` : s.agent}` } };
|
|
7621
7669
|
},
|
|
7622
7670
|
bodies: (raw, m) => [
|
|
@@ -7784,8 +7832,9 @@ async function flowDraftCmd(positional, flags, layout) {
|
|
|
7784
7832
|
return 0;
|
|
7785
7833
|
}
|
|
7786
7834
|
if (flags.yes !== true && !process.stdin.isTTY) {
|
|
7787
|
-
|
|
7788
|
-
|
|
7835
|
+
// The model call was made and charged; a script must not read that as done.
|
|
7836
|
+
console.log(` ${c.dim(`the draft call was made (charged like a step), but this is not a terminal to ask — nothing saved — run it again with --yes to save ${files.length === 1 ? "this file" : `these ${files.length} files`} (the draft will differ), or save them with \`foldrun source put\``)}\n`);
|
|
7837
|
+
return 1;
|
|
7789
7838
|
}
|
|
7790
7839
|
if (!(await confirmed(flags, "flow draft", `Save ${files.length} file${files.length === 1 ? "" : "s"} to ${ws}${files.some((f) => f.before !== null) ? " (replacing what is there)" : ""}? [y/N] `))) {
|
|
7791
7840
|
console.log(`\n ${c.dim("nothing saved")}\n`);
|