foldrun 0.9.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/foldrun.mjs +33 -11
- package/docs/agents.md +3 -1
- package/docs/api-usage.md +16 -2
- package/docs/api.md +30 -5
- package/docs/assistant.md +139 -0
- package/docs/backups-and-restore.md +35 -66
- package/docs/budgets-and-billing.md +1 -1
- package/docs/cli.md +23 -9
- package/docs/flows.md +1 -1
- package/docs/hooks.md +139 -0
- package/docs/network.md +158 -0
- package/docs/notifications.md +7 -1
- package/docs/secrets.md +54 -0
- package/docs/security.md +28 -7
- package/docs/tools.md +36 -5
- package/docs/versions.md +39 -2
- package/package.json +3 -3
- package/src/commands.mjs +904 -21
- package/src/guide.mjs +1 -1
- package/src/terminal.mjs +85 -0
package/docs/cli.md
CHANGED
|
@@ -39,7 +39,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
39
39
|
| `foldrun agent ls` | the agents deployed in a workspace — name, model, description (`GET …/agents`); `flow ls` the flows and how each fires (`GET …/flows`). `--to <workspace>`, `--json` for the list as the platform returns it |
|
|
40
40
|
| `foldrun guide` | the coding-agent rules, copied from what Next.js does: a short managed block in the account's `AGENTS.md`, between `<!-- BEGIN:foldrun-agent-rules -->` markers, telling a coding agent (Claude Code, Cursor, Codex) to read `foldrun docs` before it writes an agent, flow or tool, and a `CLAUDE.md` that imports it with `@AGENTS.md`. `foldrun init` writes them, `foldrun pull` refreshes them, and `foldrun check` re-adds them when a coding agent runs it and they are missing or old — as `next dev` does. Only the block is rewritten, in place; text around it is kept. The runtime strips the block, so the agents that run in the account never see it; the rest of `AGENTS.md` stays their shared context. `--check` exits 1 when the block is missing or old (for CI), `--print` writes it to stdout |
|
|
41
41
|
| `foldrun docs [page]` | these docs, from the copy that ships inside the CLI — the way Next.js ships `node_modules/next/dist/docs` — so a coding agent reads the version it is driving, offline. No page lists them; `foldrun docs flows` prints one; `--path` prints the folder. Design records and the platform operator's routes are left out |
|
|
42
|
-
| `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`) |
|
|
42
|
+
| `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`). Hooks: a `hooks/<name>/` folder that is not a valid hook (`on:`, `run.mjs` beside `hook.md`, `tool:` only on tool events, `timeout:` 1 to 120), and a `hooks:` name in an agent or an AGENTS.md that is in neither `hooks/` nor the library, are errors; a `tool.call` hook on an agent whose `web.search:` (or `web.fetch:`) the model provider runs on its own servers is a warning — those calls never reach the hook |
|
|
43
43
|
| `foldrun extract [dir]` | move each single-file script tool — a `tools/<name>.md` with its program in a fenced block — into a folder: `tools/<name>/tool.md` with `run:` pointing at `run.js`, `run.py` or `run.sh` beside it. Frontmatter is edited as text, never re-serialised, so the diff is only what moved. A tool that already has a folder is left for you; `--dry-run` lists what it would do |
|
|
44
44
|
| `foldrun check --to <ws>` | validate the copy that is DEPLOYED, rather than the folder in front of you — a workspace edited through the API, by an agent or by `foldrun source put`, has had no way to be checked short of spending a run. The files are fetched into a temporary folder and the ordinary check runs over them, so there is one copy of the rules; the output names the workspace and the platform it checked |
|
|
45
45
|
| `foldrun agent run <name>` | run one agent once on a platform, with no flow file: a flow of one step. `--task "…"` is the instruction and is required. Prints the run id and how to follow it; `--wait` holds on (in 25-second pieces, as `invoke` does) and then prints the same report `foldrun report` would; `--test` makes it a [test run](runs#test-runs) |
|
|
@@ -47,6 +47,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
47
47
|
| `foldrun agent import <workspace>/<agent>` | copy an agent you already have in another workspace into this one: its `agent.md` and own `skills/`, `scripts/`, `knowledge/` — never `memory/`, `outputs/` or `state/`. Into the workspace you stand in, or `--workspace <name>`; `--as <name>` imports it under another name (and rewrites `name:`), which a name clash requires. `--to <workspace>` does it on the platform (`POST /api/workspaces/<ws>/agents/import`) and lists the tools, skills, scripts and secrets the copy uses that the workspace lacks. See [Importing an agent](workspaces#importing-an-agent-you-already-have) |
|
|
48
48
|
| `foldrun agent import --list` | what could be imported into a deployed workspace: every other workspace this key may read, and each one's agents with their descriptions (`GET …/agents/import`). Imports nothing. `--to <workspace>`, `--json` |
|
|
49
49
|
| `foldrun agent unlink <agent>` | take one away: `--subagent <name>`, `--consult <name>` or `--can-ask`. An emptied list is written `key: []`, not deleted. In this folder, refused the same way, nothing written, when the file changed since it was read |
|
|
50
|
+
| `foldrun agent network <agent> [open\|trusted\|none\|inherit]` | which sites the agent's sandbox may reach ([Network](network)). Alone, prints what a run resolves — the level, where it came from, the sites added. With a level and/or `--add <site>` / `--remove <site>` (repeatable), rewrites the one `network:` line of its `agent.md` — the edit the dashboard's Network card makes — prints the diff and the new resolution; `inherit` drops the agent's own level, and a line with nothing left to say is removed. `--dry-run` writes nothing; `--to <workspace>` edits the deployed copy through `PATCH /api/workspaces/<ws>/agents/<name>` |
|
|
50
51
|
| `foldrun flow add <flow> <pattern>` | one palette block from the [flow canvas](flows#editing-on-the-canvas), as the same markdown edit the canvas writes (core's `flow-patterns`): `chain` (alias `step`: `--agent <name>`, `--instruction "…"`, `--after <n>` or `--before <n>`, default last), `parallel` (`--agent`, `--group <n>`), `router` (`--agent <triage>`, `--cases BUG=bugs,DOCS=docs`, `--else <agent>`, `--after`), `fan-out` (`--step`, `--each lines\|items\|"rows of <path>"`, `--max <n>`), `loop` (alias `evaluator`: `--step`, `--loop <n>` default 3, `--until <MARKER>` default APPROVED, `--judge "<claim>"` adds `verify: judge:`), `approval` (`--step`; the `!` marker, `approve: true` on a `?` step), `ask` (`--step`, `--question "…"`), `wait` (`--step`, `--wait 30m\|event`), `rescue` (`--step`, `--on-fail <agent>`), `subflow` (`--flow <other>`, `--after`). `--step` is the step's place in the file (1 = the first step line) or the agent it runs; `--off` takes a step pattern back off. Prints the diff, runs the rules `check` runs over the result and refuses — writing nothing — an edit that would add an error; one that was there before does not block it. `--dry-run` shows the diff and writes nothing. In this folder by default; `--to <workspace>` edits the deployed copy through the canvas's own route (`PATCH …/flows/<flow>` with `{ edit }`), and the sha256 of the file it read as `expect`: one edited meanwhile is refused (`409`), nothing written. In this folder it re-reads the file just before writing — after the y/N, after `check` — and refuses the same way, writing nothing, when it changed since it was read |
|
|
51
52
|
| `foldrun flow rm-step <flow> --step <n\|agent>` | delete one step, as the canvas's × does (core's `removeStep`): the step line and its indented options go, the groups after an emptied one renumber, prose and the agent's file are untouched. Says what else changes first (`case:`/`when:` steps that will route on the step before, the renumbering), prints the diff, refuses one `foldrun check` would call a new error, then asks — no terminal needs `--yes`. `--step` is the step's place in the file (1 = the first step line) or the agent it runs. `--dry-run` writes nothing; `--to <workspace>` sends the canvas's `PATCH …/flows/<flow>` with `{ edit: { op: "remove", step } }`, and the sha256 of the file it read as `expect`: one edited meanwhile is refused (`409`), nothing written. In this folder it re-reads the file just before writing — after the y/N, after `check` — and refuses the same way, writing nothing, when it changed since it was read |
|
|
52
53
|
| `foldrun flow dup-step <flow> --step <n\|agent>` | duplicate one step, as the canvas's Duplicate (Cmd/Ctrl+D) does (core's `duplicateStep`): a copy of the step line — its `?`/`!` marker and every indented option — goes directly under it with the same group number, so the copy runs in parallel with the original; nothing else moves. Prints the diff and refuses one `foldrun check` would call a new error; additive, so it does not ask. `--dry-run` writes nothing; `--to <workspace>` sends `PATCH …/flows/<flow>` with `{ edit: { op: "duplicate", step } }`, and the sha256 of the file it read as `expect`: one edited meanwhile is refused (`409`), nothing written. In this folder it re-reads the file just before writing — after the y/N, after `check` — and refuses the same way, writing nothing, when it changed since it was read |
|
|
@@ -60,7 +61,13 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
60
61
|
| `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 |
|
|
61
62
|
| `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" } }` |
|
|
62
63
|
| `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 |
|
|
64
|
+
| `foldrun assistant ["<message>"]` | the dashboard's **assistant**, from a terminal (also `foldrun ai`; `--to <workspace>`): `POST …/assistant` answers from the foldrun docs and the deployed workspace's files only, and writes agents, flows, tools and skills as proposals — the CLI prints the reply, the doc sections it read, every proposed file as a diff and what the check 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 (a plain question with no files exits 0); each file goes through `PUT …/source` with `ifMatch`, so one that changed meanwhile is refused. It prints the tokens the reply used and what it was charged. A proposed new workspace is created blank on yes (`--yes` on a pipe; never over one that exists); in a conversation the CLI then moves into it and carries on. Every reply is kept; `--conversation <id>` carries one on (its messages and unsaved files come back, and its workspace unless `--to` says another). With no message, in a terminal, it is a conversation: `/save`, `/files`, `/drop <path>`, `/new`, `/quit`. `--json` prints the platform's answer. Each reply is one model call, charged like a step. See [Assistant](assistant) |
|
|
65
|
+
| `foldrun assistant history` | your kept assistant conversations, newest first (`GET /api/assistant/conversations`): id, workspace, when, how many messages and unsaved files, and the first question. `--q <text>` searches them, `--workspace <name>` narrows to one, `--json` prints the answer. `foldrun assistant show <id>` prints one whole; `foldrun assistant rm <id>` deletes it (asks; `--yes`) |
|
|
63
66
|
| `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 |
|
|
67
|
+
| `foldrun hook ls` | the hooks in this workspace (`hooks/<name>/`) and the account library (`library/hooks/<name>/`): each one's `on:`, `tool:` and description, read the way a run reads them — a folder that is not a valid hook is listed with the reason and the command exits 1. A workspace hook shadows a library one of the same name. `--json` for a script. `--to <workspace>` lists the DEPLOYED workspace's hooks instead (`GET /api/workspaces/<ws>/hooks`, the dashboard's Hooks panel): who turns each on, and the denies, rewrites and failures they wrote into recent runs — `--runs <n>` reads further back (default 50, up to 500), `--all` shows every decision |
|
|
68
|
+
| `foldrun hook add <name> --on <event>` | a hook folder: `hook.md` (`on:` one of `tool.call`, `tool.result`, `step.start`, `step.end`; `tool:` from `--tool <pattern>` — `Bash`, `Write\|Edit`, `mcp__*` — on the tool events only) and a self-contained `run.mjs` that allows everything, with the whole contract in its comments: the event on stdin, one JSON answer on stdout, exit 2 to deny with stderr as the reason. `--library` writes it in the account library for every workspace; `--to <workspace>` picks one in an account. An agent turns it on with `hooks: [name]`; `hooks:` in a workspace's or the account's AGENTS.md turns it on for every agent under it |
|
|
69
|
+
| `foldrun hook test <name>` | run one hook HERE on one event, exactly as a step runs it (the event on stdin, a stripped environment, its `timeout:`), and print its answer — decision, reason, a rewritten input, context, reply or note — and how long it took. `--event '<json>'` or `--event @file`; without it, a sample event for the hook's `on:`. A deny is an answer (exit 0); a hook that crashes, times out or answers in the wrong shape is a failure (exit 1) — which, on a gate, refuses the call. `--to <workspace>` runs the DEPLOYED hook on the platform instead (`POST …/hooks/<name>/test { event }`) |
|
|
70
|
+
| `foldrun hook rm <name>` | delete a hook folder (`--library` for the account's). Says which agents and AGENTS.md files still turn it on — their steps are refused until it goes from their `hooks:` — and asks; `--yes` on a pipe |
|
|
64
71
|
| `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 |
|
|
65
72
|
| `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 |
|
|
66
73
|
| `foldrun eval new <name>` | the Evals page's New, on the deployed workspace (`POST …/evals { name }`): an eval whose `agent:` is the workspace's first agent and whose case is a placeholder — edit it (`source cat`/`put`), then `foldrun eval <name> --to <workspace>` runs it. `--to <workspace>` |
|
|
@@ -78,7 +85,8 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
78
85
|
| `foldrun stop <run-id>` | kill a run in flight: the queued job is dropped and the sandbox the current step is spending in is destroyed. It prints which step is running and what has been spent, then asks you to type the run id back unless `--yes`. Finished steps keep their results, and costs already incurred stay on the bill. A run that has already finished is said so, not stopped. With no run id and a filter — `--status running,queued`, `--since 2h`, `--until 1h` (started before then: a span ago, or a date), `--flow <name>` — it stops every run that matches (`POST …/runs/bulk`), in `--to <workspace>` or across the account: it lists what matches first, asks, and then stops exactly the runs it listed, so one that starts in between is not swept up. `--dry-run` lists and touches nothing. The route refuses more than 100 at once |
|
|
79
86
|
| `foldrun answer <run-id> "…"` | answer the question an agent is asking mid-step (`tools: [ask]`). `--option <n>` picks one of the choices it offered; `--question <id>` one of several. The agent is waiting in its sandbox and carries on at once. `foldrun approvals` lists what is being asked |
|
|
80
87
|
| `foldrun message <run-id> "…"` | say something to a running agent. It reaches the model after its next tool call, as context — it changes none of the step's tools. `--step <n>` picks one when a parallel group has several running. Platform only |
|
|
81
|
-
| `foldrun secrets set NAME` | store a secret — also `ls`, `rm`, and `status` (OAuth grant health) |
|
|
88
|
+
| `foldrun secrets set NAME` | store a secret — also `ls`, `rm`, and `status` (OAuth grant health). `--site <domain>` (repeatable, or `a,b`) binds it as it is stored |
|
|
89
|
+
| `foldrun secrets sites NAME [site…]` | bind a secret to the sites it is for: the egress proxy fills it in requests to those sites alone, and it never enters a sandbox ([bound to sites](secrets#bound-to-sites)). No sites prints what it is bound to; `--clear` unbinds. On the platform, or this machine's vault with `--local` |
|
|
82
90
|
| `foldrun secrets set NAME --kind <k>` | the other shapes the Connections form stores: `file` (`--file <path>`), `ssh` (`--host`, `--user`, `--port`, `--key-file <path>` or a prompted password), `api` (`--base-url`, `--header "Name: value"` repeatable), `service-account` (`--file <key.json>`, `--scopes`), `m2m` (a client-credentials app: `--token-url`, `--client-id`, `--scopes`, the secret prompted). On the platform, or this machine's vault with `--local` |
|
|
83
91
|
| `foldrun secrets clients` | the OAuth clients saved on the platform — what a consent runs from without typing the client again; the client secret is never shown. `clients add <name> --provider google\|github\|microsoft\|linkedin` (or `--authorize-url` / `--token-url`) `--client-id … --scopes …` saves one (secret prompted), `clients rm <name>` removes it |
|
|
84
92
|
| `foldrun connect NAME` | OAuth sign-in from the terminal, stored as an auto-refreshing secret — `--provider google\|github\|microsoft\|linkedin`; reconnecting a secret whose client is saved asks for nothing (`--new-client` to enter one) |
|
|
@@ -95,7 +103,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
95
103
|
| `foldrun workspaces show <name>` | what is deployed in one workspace (`GET /api/workspaces/<name>`): its agents with model and description, its flows and how each fires, and its files counted by top-level folder. `--json` for the document |
|
|
96
104
|
| `foldrun workspaces new <name> --platform` | the dashboard's **Create workspace**: blank — an `AGENTS.md` and nothing to delete (`POST /api/workspaces { name, template: true }`) — or `--starter` for the example researcher, writer and publish flow (`{ name, starter: true }`). The plan's workspace count applies. Without `--platform` it makes the folder here, as `foldrun new` does |
|
|
97
105
|
| `foldrun workspaces demo` | **Try the demo pipeline**: makes the `demo-pipeline` workspace on the platform — a four-step flow with a parallel pair, an approval gate and a pretend CRM tool (`POST /api/workspaces { demo: true }`) |
|
|
98
|
-
| `foldrun library` | the account's shared library on the platform, as the Library page shows it: `ls [skills\|tools\|scripts\|knowledge\|memory]`, `cat <kind>/<path>`, `put <kind>/<path>` (from `--file`, else stdin), `new <kind>/<name>` (the Library page's New, `POST /api/library/<kind> { name, template, language }`: the platform's template — `--template` a transport or a gallery entry, `--language` for a script tool), `rm <kind>/<path>` (asks). `deploy` still pushes the whole of it |
|
|
106
|
+
| `foldrun library` | the account's shared library on the platform, as the Library page shows it: `ls [skills\|tools\|hooks\|scripts\|knowledge\|memory]`, `cat <kind>/<path>`, `put <kind>/<path>` (from `--file`, else stdin), `new <kind>/<name>` (the Library page's New, `POST /api/library/<kind> { name, template, language }`: the platform's template — `--template` a transport or a gallery entry, `--language` for a script tool), `rm <kind>/<path>` (asks). `deploy` still pushes the whole of it |
|
|
99
107
|
| `foldrun find <words>` | the dashboard's ⌘K search: workspaces, agents, flows, tools, skills, knowledge, memory and runs across the account, ranked the same way (`GET /api/search`; `--json`) |
|
|
100
108
|
| `foldrun invoke <flow>` | start a flow on a running platform — `--watch` follows its trace here; `--test` makes it a [test run](runs#test-runs) `--once <key>` sends the key as the run's idempotency key, so a CI step that retries after a dropped response is answered with the run it already started instead of paying for a second. `--tag <t>`, repeatable, labels the run (the API's `tags`). `--inputs <name>` sends a saved input set as the task — a case of `evals/<flow>-inputs.md` or of an eval that runs the flow, as the platform has it (see [Run with saved inputs](flows#run-with-saved-inputs)). `foldrun flow run <flow>` is the same command |
|
|
101
109
|
| `foldrun source <verb>` | one workspace file on a platform: `ls [dir]`, `cat <path>`, `put <path>` (from `--file`, else stdin; `--message` goes on the revision), `new <kind> <name>`, `mv <from> <to>`, `rm <path>` — the editor's own door, so every write is a revision. `new` is the dashboard's New button for any document (`POST …/assets { kind, name, agent?, template?, language? }`): `tools`, `knowledge`, `memory`, `skills`, `scripts` (and `agents`, `flows`, `evals`), the template written by the platform; `--agent <name>` puts a knowledge, memory, skill or script document inside that agent. For a whole tree, `deploy` |
|
|
@@ -108,8 +116,8 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
108
116
|
| `foldrun onboarding` | the account's getting-started steps — workspace, agent, a run, a run that completes, a secret, a schedule or webhook, a teammate, billing where the plan asks for it, the CLI — each worked out from what is really there, with the next one first and where to do it (`--json`). The dashboard card is hidden per person from the dashboard |
|
|
109
117
|
| `foldrun preferences` | your own settings on the platform — today the theme (`system`, `light`, `dark`), kept with you on every browser; `preferences set theme dark` changes it (`/api/me/preferences`; a key acts for whoever minted it) |
|
|
110
118
|
| `foldrun api spec` | the platform's OpenAPI 3.1 document, from `GET /api/openapi.json` (no key needed) — to stdout, or `--out <file>`. Every other command sends `Foldrun-Version` (the API version this CLI was built for), an `Idempotency-Key` on each write (reused on its own retry), and waits out one `429` for `Retry-After` before saying so. See [Using the API](api-usage) |
|
|
111
|
-
| `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
|
|
112
|
-
| `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`) |
|
|
119
|
+
| `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, the rate limits, and the last 30 days: requests by version, and any deprecated route the account still calls — by which key or person, and when it stops answering. `api version pin <YYYY-MM-DD>` pins it, `unpin` goes back to current (`PATCH /api/account/api`; an admin's) |
|
|
120
|
+
| `foldrun changelog` | What's new: first the API — its current version, the breaking changes each dated version brought, and the routes deprecated with their sunset dates — then 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`) |
|
|
113
121
|
| `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) |
|
|
114
122
|
| `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 |
|
|
115
123
|
| `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 |
|
|
@@ -123,7 +131,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
123
131
|
| `foldrun billing` | the account's balance, whether billing is on at all, what the platform is currently waiving, and the recent ledger entries with what each was for — a run names its workspace, flow and run id; an adjustment carries its own note. `--limit <n>` shows more. `statement [YYYY-MM]` is a month of the ledger (this month by default) — `--csv` for the file an accountant wants, to stdout or `--file <path>`. `wallet` is the burn, the runway and auto top-up; `wallet set auto-top-up --threshold <usd> --amount <usd>` turns it on (a refill of 5–500), `set auto-top-up off` turns it off, `set email <address>` is where receipts go. `details` is who invoices are made out to; `details set --name "…" --abn … --address "line1, city, state, postcode, AU" --email …` changes it, merged with what is there. `portal` prints a link to Stripe's billing portal. Changing the wallet, the details, and the portal are the account owner's; anyone else is told so |
|
|
124
132
|
| `foldrun billing plans` | the Plans page: every plan with its monthly fee, credits, runs at once and workspaces, which one the account is on and this cycle's credits left (`GET /api/billing/plans`). `billing plan <id>` chooses one — a first plan answers with Stripe's checkout page, a change on a live plan is prorated at Stripe at once (asks first; `--yes`); `plan cancel` stops it at the end of the cycle, `plan resume` undoes that until then (`POST`/`DELETE /api/billing/subscribe`) |
|
|
125
133
|
| `foldrun billing top-up <usd>` | the Payment page's top-up: Stripe's checkout page for that amount, opened in the browser (`--no-browser`, or no terminal, prints it). The credit lands when Stripe confirms the payment. `billing card` is the same for saving a card for auto top-up. Both need `billing:manage` |
|
|
126
|
-
| `foldrun billing credit <usd>` | put credit on the balance by hand (`POST /api/billing { usd, note }`, `--note "…"` on the ledger line) — only on an install without Stripe, a self-hosted or dev platform; once billing is live the platform refuses it and top-ups go through `billing top-up`. Needs `billing:manage` |
|
|
134
|
+
| `foldrun billing credit <usd>` | put credit on the balance by hand (`POST /api/billing { usd, note }`, `--note "…"` on the ledger line) — only on an install without Stripe, a self-hosted or dev platform; once billing is live the platform refuses it and top-ups go through `billing top-up`. The dashboard's Billing → Add credit does the same while billing is off. Needs `billing:manage` |
|
|
127
135
|
| `foldrun gallery` | the tools the platform ships to every account (`web`), and for each whether this account keeps its own copy and whether that copy still matches. `pull` lays the shelf down under `~/.foldrun/gallery/<host>/` for offline runs (`foldrun run` and `foldrun eval` refresh it themselves before each run); `upgrade <tool>` replaces the account's copy with the current gallery version, the old one a revision back |
|
|
128
136
|
| `foldrun account` | the account's own defaults — the AGENTS.md frontmatter every workspace under it inherits: `timezone`, `notify`, `budget`, `concurrency`. `foldrun account set timezone Australia/Sydney`, `set notify email you@example.com` (`--events failed,awaiting-approval,completed`), `set notify url https://…`, `set budget 60/month`, `set concurrency 4`, and `clear <key>` to unset one. `set notify` merges against what is there, because the platform replaces the whole block. `providers` lists the model providers the account's files use and the last daily key check — `--check` asks now, and a dead key is a non-zero exit. `model` is the account's own model key, what every step runs on unless a `provider:` block names another: `foldrun account model` shows it (never the key, only its last four; exit 1 when there is none and runs are refused), `model set <provider> --key <api key>` sets it — a preset name (anthropic, openrouter, openai, groq, …) or `custom --base-url https://… --format anthropic|openai`, `--key -` reads the key from stdin — and `model remove` removes it; a Claude login token is refused for any account not allowed the platform's own credential. `export` downloads everything the platform holds about the account — workspaces, runs, members and the whole ledger — as one JSON file (owner only; `--file <path>`, `--force`). Singular — `accounts` below is the list of logins on this machine |
|
|
129
137
|
| `foldrun workspace` | one deployed workspace's own settings (`--to <workspace>`): description, timezone, budget and notify, read from its `AGENTS.md` — unset means the account's. `set timezone <tz>`, `set budget 20/week`, `set notify email <address>` / `set notify url <https://…>` (`--events`; merged with what the file says), `set description "…"`, `set name <new>` — a rename changes every webhook URL, so it asks first and names the flows whose URLs changed — and `clear <key>`. Singular — `workspaces` is the list |
|
|
@@ -188,12 +196,14 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
188
196
|
| `--step <n>` | `approve`, `reject`: decide one gate, numbered as `report` prints it. Without it, every step of that run which is waiting |
|
|
189
197
|
| `--payload <json\|text\|@file>` | `approve`: release a step waiting on `wait: event` with this body (`POST …/approve` `{ payload }`). JSON is sent as JSON, anything else as text; `@file` reads a file |
|
|
190
198
|
| `--note "<text>"` | `approve`: guidance the approved step reads in its prompt — "approve, but skip the Sydney batch". `reject`: the reason, recorded on the trace. `billing credit`: the ledger line's note |
|
|
191
|
-
| `--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 |
|
|
192
|
-
| `--json` | `report`: the raw run record instead of the report. `observe`, `usage`, `workspaces`, `workspaces show`, `agent ls`, `flow ls`, `agent import --list`, `backups requests`, `status --platform --history`, `team`: the raw document |
|
|
199
|
+
| `--yes` | skip the confirmation, deliberately: `approve`, `stop`, `deploy` (when it would delete files), and a bulk `stop` or `rerun`, `flow rotate-hook`, `flow draft`, `assistant`, `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 |
|
|
200
|
+
| `--json` | `report`: the raw run record instead of the report. `observe`, `usage`, `workspaces`, `workspaces show`, `agent ls`, `flow ls`, `agent import --list`, `assistant`, `backups requests`, `status --platform --history`, `team`: the raw document |
|
|
193
201
|
| `--events <a,b>` | `account set notify`: which events are notified on — `failed`, `awaiting-approval`, `completed` |
|
|
194
202
|
| `--watch` | `invoke`: follow the run's trace here as it happens; the exit code is the run's |
|
|
195
203
|
| `--print` | `open`: print the URL, do not open it |
|
|
196
204
|
| `--value "<text>"` | `secrets set`: skip the prompt — careful with shell history |
|
|
205
|
+
| `--site <domain>`, `--clear` | `secrets set`, `secrets sites`: a site the secret is bound to (repeatable); `--clear` unbinds |
|
|
206
|
+
| `--add <site>`, `--remove <site>` | `agent network`: a site to add to, or take off, the agent's `network:` list (repeatable) |
|
|
197
207
|
| `--account` | `secrets`: account scope rather than this workspace's — the workspace is the folder's name (`--workspace`), local or on the platform alike |
|
|
198
208
|
| `--wait` | `invoke`, `agent run`: hold on and print the result. Asked in 25-second pieces, so a flow that runs for many minutes is never cut by whatever sits in front of the platform; a run parked on a person prints its page and exits 2 |
|
|
199
209
|
| `--from <n>` | `invoke`, `flow run`, `rerun`: start at step n; earlier steps are recorded as skipped. A bulk `rerun` defaults to 1. (One flag, two meanings: a template on `init`, a step everywhere else) |
|
|
@@ -212,11 +222,15 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
212
222
|
| `--platform` | `workspaces new`, `agent new`, `flow new`, `tool new`: make it on the platform, not in this folder. `status`: the platform's own health (`--history` for the daily record) |
|
|
213
223
|
| `--description`, `--model`, `--pattern`, `--template` | `agent new --platform`: its description and model tier. `flow new --platform`: the shape to start from. `tool new --platform`, `source new`, `library new`: a transport or gallery entry to start from |
|
|
214
224
|
| `--list` | `agent import`: what could be imported, importing nothing |
|
|
225
|
+
| `--on <event>` | `hook add`: what the hook listens for — `tool.call`, `tool.result`, `step.start` or `step.end` |
|
|
226
|
+
| `--tool <pattern>` | `hook add`: which tools a `tool.call` or `tool.result` hook sees — `Bash`, `Write\|Edit`, `mcp__*`; all of them when unset |
|
|
227
|
+
| `--library` | `hook add`, `hook rm`: the account library's `library/hooks/`, not this workspace's |
|
|
228
|
+
| `--event <json\|@file>` | `hook test`: the event the hook reads on stdin (default: a sample for its `on:`) |
|
|
215
229
|
| `--path <p>` | `tool test`: the path an http tool should probe, appended to its `base:` — a path that escapes the base is refused, the same confinement the agent gets |
|
|
216
230
|
| `--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 |
|
|
217
231
|
| `--local` | `deploy`, `secrets`, `logs`: the installation on this machine, even when signed in |
|
|
218
232
|
| `--quiet` | leave out the dim "acting as …" line a platform command prints on stderr |
|
|
219
|
-
| `--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 |
|
|
233
|
+
| `--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`, `assistant`, `history restore`, `agent link`/`unlink`, `agent network`: show the diff, write nothing. `restore`: the diff only, restore nothing. A bulk `stop`/`rerun`: list what matches, touch nothing |
|
|
220
234
|
| `--out <file>` | `api spec`: where to write the OpenAPI document (default stdout) |
|
|
221
235
|
| `--file <path>` | `source put`: the file to send. `storage get`, `report get`, `account export`, `billing statement --csv`: where to write — `-` for stdout |
|
|
222
236
|
|
package/docs/flows.md
CHANGED
|
@@ -112,7 +112,7 @@ begins.
|
|
|
112
112
|
| `ask:` | the same gate carrying a question; the typed answer reaches the prompt (a plain approval with no note reaches it as "approved with no note") |
|
|
113
113
|
| `preview:` | what the gate shows: paths under `storage/` (a `workspace/storage/` prefix is accepted), comma-separated, globs allowed — `draft/*.mdx, draft/images/*.webp`; at most 12 files |
|
|
114
114
|
| `wait:` | `3d` (`s`/`m`/`h`/`d`, capped at 30 days) or `event` — the run page shows the event URL and can release it with a payload, recorded as released by you (approve with `payload`) |
|
|
115
|
-
| `on-fail:` | another agent takes the step over, with the failure as context. The rescuer owes what the step owed: its `verify
|
|
115
|
+
| `on-fail:` | another agent takes the step over, with the failure as context — and, for a gated step, the person's approval and answer, which it acts within. The rescuer owes what the step owed: its `verify:`, its `output:`, and a `loop:` step's `until:` marker (a loop that runs out of cycles goes to its rescuer too). A `verify:` that accepts `BLOCKED` never fails, so it never reaches the rescuer |
|
|
116
116
|
| `delegate:` | the step's agent picks who runs next, from this set |
|
|
117
117
|
|
|
118
118
|
### What is retried without a `retry:`
|
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Hooks
|
|
2
|
+
|
|
3
|
+
A hook is your own code at the points every step passes through: before a
|
|
4
|
+
tool runs, after it ran, when a step starts and when it ends. It can refuse
|
|
5
|
+
a call, rewrite it, add context, or rewrite the reply, and it does the same
|
|
6
|
+
thing on every model: Claude, or any other provider through the translator.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
workspaces/<name>/hooks/no-force-push/
|
|
10
|
+
hook.md when it runs
|
|
11
|
+
run.mjs what it does
|
|
12
|
+
library/hooks/<name>/ the same, shared by every workspace
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Turning one on
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# agents/deployer/agent.md
|
|
19
|
+
hooks: [no-force-push]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`hooks:` in an agent's frontmatter turns hooks on for that agent. The same
|
|
23
|
+
key in a workspace's `AGENTS.md` turns them on for every agent in it, and in
|
|
24
|
+
the account's `AGENTS.md` for every agent in every workspace. They run in
|
|
25
|
+
that order (account, workspace, agent), each name once. A name is looked up
|
|
26
|
+
in the workspace's `hooks/` first, then the library's. A name that resolves
|
|
27
|
+
to nothing is a `foldrun check` error, and a step that names one is refused
|
|
28
|
+
rather than run without its hooks.
|
|
29
|
+
|
|
30
|
+
## hook.md
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
---
|
|
34
|
+
on: tool.call # tool.call | tool.result | step.start | step.end
|
|
35
|
+
tool: Bash # which tools; Write|Edit, mcp__*, a short name like web
|
|
36
|
+
timeout: 30 # seconds, 1-120 (30 for tool.call/step.start, 60 otherwise)
|
|
37
|
+
description: Refuses force pushes and skipped git hooks.
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
Why this hook exists, for the next person who reads it.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| `on:` | when | it can |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `tool.call` | before a tool runs | allow it, refuse it, or rewrite its input |
|
|
46
|
+
| `tool.result` | after a tool ran | see the input and the result; leave a note on the trail |
|
|
47
|
+
| `step.start` | before the model starts | add context to the task, or refuse the step |
|
|
48
|
+
| `step.end` | after the reply | see it, or replace it |
|
|
49
|
+
|
|
50
|
+
## run.mjs
|
|
51
|
+
|
|
52
|
+
One self-contained ES module, using Node's built-ins only (no relative
|
|
53
|
+
imports), at most 100 KB. The event arrives as one JSON object on stdin, and
|
|
54
|
+
the answer is the last line of JSON the hook prints:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
let raw = "";
|
|
58
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
59
|
+
const event = JSON.parse(raw);
|
|
60
|
+
// { event: "tool.call", hook, run_id, step, agent, workspace,
|
|
61
|
+
// tool: { name: "Bash", input: { command: "git push --force" } }, subagent? }
|
|
62
|
+
|
|
63
|
+
const cmd = String(event.tool?.input?.command ?? "");
|
|
64
|
+
if (/\bgit\s+push\b.*\s(--force|-f)(\s|$)/.test(cmd)) {
|
|
65
|
+
console.log(JSON.stringify({ decision: "deny", reason: "force push — use --force-with-lease" }));
|
|
66
|
+
} else {
|
|
67
|
+
console.log("{}");
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| event | answer |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `tool.call` | `{}` or `{"decision":"allow"}` lets it through; `{"decision":"deny","reason":"…"}` refuses it (the model reads the reason); `{"input":{…}}` replaces the tool's whole input |
|
|
74
|
+
| `tool.result` | `{}`, or `{"note":"…"}` for the trail |
|
|
75
|
+
| `step.start` | `{"context":"…"}` is added to the task; exit 2 refuses the step |
|
|
76
|
+
| `step.end` | `{"reply":"…"}` replaces the reply |
|
|
77
|
+
|
|
78
|
+
Exit code 2 refuses too, with what the hook printed to stderr as the reason,
|
|
79
|
+
which is the convention most agent tools share.
|
|
80
|
+
|
|
81
|
+
## When a hook goes wrong
|
|
82
|
+
|
|
83
|
+
A hook that guards (`tool.call`, `step.start`) and throws, times out, exits
|
|
84
|
+
with anything but 0 or 2, or answers with the wrong shape **refuses the
|
|
85
|
+
call**. A guard that crashes must not become an open door. A hook that
|
|
86
|
+
watches (`tool.result`, `step.end`) and fails is recorded, and the step goes
|
|
87
|
+
on, because what it was watching has already happened.
|
|
88
|
+
|
|
89
|
+
When several hooks match one call they run in order. The first refusal ends
|
|
90
|
+
it, and a rewrite is handed to the next hook, so the most restrictive answer
|
|
91
|
+
wins.
|
|
92
|
+
|
|
93
|
+
## What a hook can reach
|
|
94
|
+
|
|
95
|
+
No more than the agent. A hook runs inside the step's own sandbox, as a child
|
|
96
|
+
of the step, in the agent's directory, with an environment stripped to `PATH`
|
|
97
|
+
and the locale: no secrets, no model key, and a home folder of its own. Its
|
|
98
|
+
code is read when the step starts, so an agent that edits `run.mjs` mid-step
|
|
99
|
+
changes nothing that runs, and a sandbox never writes `hooks/` back.
|
|
100
|
+
|
|
101
|
+
A rewritten tool call goes through the same checks as the original (the
|
|
102
|
+
shell and path confinement, the agent's granted tools, its sub-agents), so a
|
|
103
|
+
hook can narrow what an agent does but never widen it.
|
|
104
|
+
|
|
105
|
+
## What a hook does not see
|
|
106
|
+
|
|
107
|
+
Tools the model provider runs on its own side never pass foldrun's dispatch.
|
|
108
|
+
With `web: {search: anthropic}` (or another provider's search, or
|
|
109
|
+
`fetch: anthropic`) the search runs at the provider, and a `tool.call` hook
|
|
110
|
+
cannot refuse or rewrite it. foldrun's own `web` tool, the default, goes
|
|
111
|
+
through hooks like every other tool. `foldrun check` warns when an agent with
|
|
112
|
+
a `tool.call` hook uses a provider's own search.
|
|
113
|
+
|
|
114
|
+
## On the run page
|
|
115
|
+
|
|
116
|
+
Every refusal, rewrite, added context and failure is a line on the step's
|
|
117
|
+
trail (`hook no-force-push denied Bash: force push — use --force-with-lease`)
|
|
118
|
+
and a badge on the run page. The workspace's Hooks page lists its hooks and
|
|
119
|
+
what each one refused or rewrote lately.
|
|
120
|
+
|
|
121
|
+
## Trying one
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
foldrun hook add no-force-push --on tool.call --tool Bash
|
|
125
|
+
foldrun hook test no-force-push --event '{"tool":{"name":"Bash","input":{"command":"git push -f"}}}'
|
|
126
|
+
foldrun check
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`foldrun hook test` runs the hook on your machine exactly as a step runs it.
|
|
130
|
+
`POST /api/workspaces/<ws>/hooks/<name>/test` does the same on the platform
|
|
131
|
+
(see [API](api)).
|
|
132
|
+
|
|
133
|
+
## Two in the library to copy
|
|
134
|
+
|
|
135
|
+
- **no-force-push** refuses `git push --force` or `-f` (allowing
|
|
136
|
+
`--force-with-lease`), and `--no-verify` or `SKIP_HOOKS=1`.
|
|
137
|
+
- **external-write-log**: when the shell calls `curl` to change something on
|
|
138
|
+
an outside service (POST, PUT, PATCH, DELETE), it writes a line on the
|
|
139
|
+
trail.
|
package/docs/network.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Network
|
|
2
|
+
|
|
3
|
+
Every step runs in a sandbox of its own, and `network:` says which sites that
|
|
4
|
+
sandbox may reach. An agent that reads the open web may reach any public
|
|
5
|
+
site; every other agent reaches a list of trusted sites plus whatever it
|
|
6
|
+
declares. Least access by default, widened on purpose — the same default
|
|
7
|
+
Anthropic's own cloud sandboxes start from.
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
---
|
|
11
|
+
name: crm-sync
|
|
12
|
+
tools: [crm_sync]
|
|
13
|
+
network: [api.hubapi.com] # the trusted list, plus this
|
|
14
|
+
---
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## The three levels
|
|
18
|
+
|
|
19
|
+
| level | the sandbox may reach |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `open` | any public site |
|
|
22
|
+
| `trusted` | the trusted list, the sites you add, and what the step declares |
|
|
23
|
+
| `none` | only the sites you add and what the step declares |
|
|
24
|
+
|
|
25
|
+
**What the step declares** is always allowed, at every level: the hosts of
|
|
26
|
+
its `apis:`, its remote MCP servers, its web providers (`web: {search: brave}`),
|
|
27
|
+
and the sites its [secrets are bound to](secrets#bound-to-sites). You never list
|
|
28
|
+
those twice.
|
|
29
|
+
|
|
30
|
+
No level is ever a way into private space: the cluster, the LAN and the
|
|
31
|
+
cloud metadata endpoint are refused whatever `network:` says.
|
|
32
|
+
|
|
33
|
+
## The default
|
|
34
|
+
|
|
35
|
+
Unset, the level follows what the agent does:
|
|
36
|
+
|
|
37
|
+
- an agent with the `web` tool that may **fetch or browse** is `open` — a search
|
|
38
|
+
result can name any site, and a list would break it;
|
|
39
|
+
- every other agent is `trusted`.
|
|
40
|
+
|
|
41
|
+
An agent whose `web:` allows only `search` (`web: {actions: [search]}`) is
|
|
42
|
+
`trusted`: it reads results, not pages.
|
|
43
|
+
|
|
44
|
+
## The trusted list
|
|
45
|
+
|
|
46
|
+
Package registries, source hosts and cloud SDKs — the domains Anthropic's
|
|
47
|
+
Claude Code cloud environments allow at their default *Trusted* level, plus
|
|
48
|
+
the Debian mirrors the runner image installs `runtime: system:` packages
|
|
49
|
+
from. Among them:
|
|
50
|
+
|
|
51
|
+
| | |
|
|
52
|
+
|---|---|
|
|
53
|
+
| Anthropic | api.anthropic.com, claude.ai, docs.claude.com, … |
|
|
54
|
+
| Source | github.com, raw.githubusercontent.com, gitlab.com, bitbucket.org, … |
|
|
55
|
+
| Packages | registry.npmjs.org, pypi.org, files.pythonhosted.org, crates.io, rubygems.org, proxy.golang.org, Maven, NuGet, … |
|
|
56
|
+
| Containers | registry-1.docker.io, ghcr.io, gcr.io, public.ecr.aws, … |
|
|
57
|
+
| Cloud | \*.googleapis.com, \*.amazonaws.com, \*.microsoftonline.com, … |
|
|
58
|
+
| Linux | deb.debian.org, security.debian.org, archive.ubuntu.com, … |
|
|
59
|
+
|
|
60
|
+
The whole list is `TRUSTED_SITES` in `@foldrun/core/network`; `foldrun agent
|
|
61
|
+
network <agent>` says how many sites it holds. It includes wide wildcards
|
|
62
|
+
such as `*.amazonaws.com` because SDKs need them — which means a bucket
|
|
63
|
+
anyone can create is reachable too. An agent handling something sensitive
|
|
64
|
+
can drop the list: `defaults: false`.
|
|
65
|
+
|
|
66
|
+
## Writing it
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
network: trusted # a level
|
|
70
|
+
network: [api.example.com, "*.example.org"] # add sites; the level stays
|
|
71
|
+
network:
|
|
72
|
+
level: none
|
|
73
|
+
allow: [api.example.com]
|
|
74
|
+
defaults: false # trusted without the trusted list
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A site is a domain or `*.domain` — no scheme, no path, no port; every port
|
|
78
|
+
of the host is covered. `*.example.com` covers `api.example.com`,
|
|
79
|
+
`a.b.example.com` and `example.com` itself. Quote a wildcard in YAML.
|
|
80
|
+
|
|
81
|
+
It is read on the agent, in the workspace's `AGENTS.md` and in the account's
|
|
82
|
+
`AGENTS.md`. The level and `defaults` are nearest-wins — the agent's, else the
|
|
83
|
+
workspace's, else the account's. The `allow` lists add up across all three,
|
|
84
|
+
so a site every agent needs is written once, in the account's `AGENTS.md`.
|
|
85
|
+
|
|
86
|
+
`foldrun check` and every deploy refuse a value they cannot read — a level
|
|
87
|
+
that is not one, a site with `https://` or a path — because it would leave
|
|
88
|
+
the agent on the level above it, wider or narrower than you wrote.
|
|
89
|
+
|
|
90
|
+
## Changing it
|
|
91
|
+
|
|
92
|
+
On the agent's page in the dashboard, the **Network** card shows the level a
|
|
93
|
+
run resolves, where it came from, and the sites on it; **Change** sets the
|
|
94
|
+
level and the sites to add. From the terminal:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
foldrun agent network crm-sync # what a run resolves
|
|
98
|
+
foldrun agent network crm-sync --add api.hubapi.com # add a site
|
|
99
|
+
foldrun agent network crm-sync none --remove a.com # a level, and take one off
|
|
100
|
+
foldrun agent network crm-sync inherit # back to the workspace's or the default
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Both write the one `network:` line of `agent.md` and nothing else. Over the
|
|
104
|
+
API it is `PATCH /api/workspaces/<ws>/agents/<agent>` with `{ "network": … }`
|
|
105
|
+
(`null` inherits).
|
|
106
|
+
|
|
107
|
+
## How it is held
|
|
108
|
+
|
|
109
|
+
Every program in the sandbox reaches the internet through the **egress
|
|
110
|
+
proxy**: the pod's `HTTPS_PROXY` and `HTTP_PROXY` name it, with the step's
|
|
111
|
+
own lease as the password, and `NODE_USE_ENV_PROXY=1` turns that on for Node.
|
|
112
|
+
curl, Python's `requests` and `httpx`, pip, npm, git, apt and the Agent SDK
|
|
113
|
+
all read those variables. The proxy holds the step's list and checks every
|
|
114
|
+
connection against it:
|
|
115
|
+
|
|
116
|
+
- a site **off the list** is refused — `403`, saying which site and what to
|
|
117
|
+
add — and the refusal is a line in the run trace;
|
|
118
|
+
- a site **on the list** is a tunnel: the bytes pass through and the proxy
|
|
119
|
+
never reads them;
|
|
120
|
+
- a site a **secret is bound to** is intercepted, so the secret can be filled
|
|
121
|
+
in on the way out ([Secrets](secrets#bound-to-sites)).
|
|
122
|
+
|
|
123
|
+
On a cluster the pod's network policy backs this up. A step whose level is
|
|
124
|
+
`trusted` or `none` runs in a pod that may reach the proxy and DNS and
|
|
125
|
+
nothing else, so a program that ignores the proxy variables finds no route
|
|
126
|
+
instead of going round the list; it cannot reach the account's browser or
|
|
127
|
+
search pods either, which are the world fetched on a run's behalf.
|
|
128
|
+
|
|
129
|
+
The trace opens with the step's network:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
network: trusted (default) — the trusted list (207 sites); added: api.hubapi.com
|
|
133
|
+
egress: CONNECT pypi.org:443 × 3 (840ms total)
|
|
134
|
+
network: example.com is not a site this step may reach (network: trusted). Add it to the agent's network: list — network: [example.com] — or set network: open. Refused.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Report-only, while an install learns
|
|
138
|
+
|
|
139
|
+
An install enforces the list when its worker has `FOLDRUN_NETWORK_ENFORCE=1`.
|
|
140
|
+
Until then the proxy only **reports**: a site off the list is allowed, and
|
|
141
|
+
the trace names it once per step —
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
network: api.example.com would be refused (network: trusted) — allowed, because this install only reports. Add it to the agent's network: list to keep it reachable.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
— so the sites your agents really use can be added before anything is
|
|
148
|
+
refused. The trace line `network: … — report only on this install` says
|
|
149
|
+
which mode a step ran in.
|
|
150
|
+
|
|
151
|
+
### Where it is not held
|
|
152
|
+
|
|
153
|
+
`foldrun run` on your own machine has no sandbox, and a compose install with
|
|
154
|
+
no egress proxy (no `FOLDRUN_EGRESS_URL`) has no forward door: there,
|
|
155
|
+
`network:` is checked and resolved but not enforced, and the trace says
|
|
156
|
+
`not enforced here`. On the single-container tier the proxy variables are
|
|
157
|
+
set and the proxy holds the list, but the container's network is not walled
|
|
158
|
+
in, so a program that ignores the variables still goes direct.
|
package/docs/notifications.md
CHANGED
|
@@ -140,6 +140,12 @@ The body, and the webhook's JSON:
|
|
|
140
140
|
that wants the data. A webhook URL is the one integration: those services,
|
|
141
141
|
and every phone-push relay, all accept a POST of that shape.
|
|
142
142
|
|
|
143
|
+
Every delivery says which shape its body has, in `x-foldrun-webhook-version`
|
|
144
|
+
(today `1`, also in the `foldrun-webhooks/1` user agent). Within a version the
|
|
145
|
+
body only gains fields, so ignore the ones you do not know. A field removed,
|
|
146
|
+
renamed or given another type is a new version, announced in What's new at
|
|
147
|
+
least six months before the old shape stops being sent.
|
|
148
|
+
|
|
143
149
|
## Webhook deliveries, retries and redelivery
|
|
144
150
|
|
|
145
151
|
Every webhook send is a **delivery**: recorded first, attempted at once,
|
|
@@ -290,7 +296,7 @@ sending, so it holds for every mail, from every part of the platform.
|
|
|
290
296
|
| `security` | password resets, email confirmations, a login moved, an account made for you | no |
|
|
291
297
|
| `invites` | an invitation to join an account | no |
|
|
292
298
|
| `billing` | a payment failed, and the grace before runs stop | no |
|
|
293
|
-
| `account` | a notification webhook switched off after days of failures; a close request | no |
|
|
299
|
+
| `account` | a notification webhook switched off after days of failures; a close request; an API route the account still calls that is being switched off | no |
|
|
294
300
|
| `run-alerts` | a workspace's `notify:` email: failed, completed, blocked, quarantined, `sla`, `budget` | yes — per workspace |
|
|
295
301
|
| `approvals` | a run waiting for you, with approve and reject links | yes — per workspace |
|
|
296
302
|
| `low-balance` | credits below the warning level | yes |
|
package/docs/secrets.md
CHANGED
|
@@ -12,6 +12,7 @@ git — which is what makes a workspace safe to commit.
|
|
|
12
12
|
```sh
|
|
13
13
|
foldrun secrets set STRIPE_KEY # this workspace's; prompted, never echoed
|
|
14
14
|
foldrun secrets set STRIPE_KEY --account # the account's, shared by every workspace
|
|
15
|
+
foldrun secrets set STRIPE_KEY --site api.stripe.com # bound: never in a sandbox (below)
|
|
15
16
|
foldrun secrets ls
|
|
16
17
|
```
|
|
17
18
|
|
|
@@ -112,6 +113,59 @@ land on any worker behind the Service and still find its lease. Without
|
|
|
112
113
|
a proxy configured (the CLI, a compose install without
|
|
113
114
|
`FOLDRUN_EGRESS_URL`) a step materialises everything, as it always did.
|
|
114
115
|
|
|
116
|
+
## Bound to sites
|
|
117
|
+
|
|
118
|
+
A secret can be **bound** to the sites it is for. A bound secret never
|
|
119
|
+
enters a sandbox, whatever the step grants — Bash, a script tool, a browser.
|
|
120
|
+
The sandbox holds `${NAME}`; the [egress proxy](network#how-it-is-held), which
|
|
121
|
+
every program's HTTPS goes through, fills it in requests to those sites and
|
|
122
|
+
no other. This is how Anthropic's cloud sandboxes attach a network secret,
|
|
123
|
+
and it closes the gap above: a step with Bash used to get every declared
|
|
124
|
+
secret's real value.
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
foldrun secrets sites DATAFORSEO_PASSWORD api.dataforseo.com
|
|
128
|
+
foldrun secrets sites DATAFORSEO_PASSWORD # what it is bound to
|
|
129
|
+
foldrun secrets sites DATAFORSEO_PASSWORD --clear # unbind it
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or **Bind to sites** under the secret in Settings → Secrets, or over the API:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
PATCH /api/secrets { "name": "DATAFORSEO_PASSWORD", "sites": ["api.dataforseo.com"] }
|
|
136
|
+
PUT /api/secrets { "name": "…", "value": "…", "sites": ["api.dataforseo.com"] } # set and bind at once
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
What a program in the sandbox writes is unchanged; it sends the placeholder
|
|
140
|
+
where the value would go:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
curl -H "Authorization: Bearer $HUBSPOT_TOKEN" https://api.hubapi.com/crm/v3/objects/contacts
|
|
144
|
+
curl -u "$DATAFORSEO_LOGIN:$DATAFORSEO_PASSWORD" https://api.dataforseo.com/v3/serp/...
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The proxy fills `${NAME}` in the request's URL and headers, and inside a
|
|
148
|
+
`Basic` Authorization header (which `curl -u` sends base64'd). To do that
|
|
149
|
+
for a bound site it ends the TLS connection itself, with a certificate from
|
|
150
|
+
the install's **egress CA**, which the sandbox trusts: the pod's start puts
|
|
151
|
+
that CA in a bundle and points `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`,
|
|
152
|
+
`CURL_CA_BUNDLE`, `PIP_CERT`, `GIT_SSL_CAINFO` and `NODE_EXTRA_CA_CERTS` at
|
|
153
|
+
it. Only bound sites are intercepted; every other site is a tunnel the
|
|
154
|
+
proxy cannot read. The CA is derived from the install's key, so every
|
|
155
|
+
worker presents the same one, and rotating the key rotates it.
|
|
156
|
+
|
|
157
|
+
A bound site is always reachable, at every [network](network) level.
|
|
158
|
+
|
|
159
|
+
Leave a secret unbound when the program needs the value itself rather than
|
|
160
|
+
in a request: one that signs requests with it (an HMAC, AWS SigV4), puts it
|
|
161
|
+
in a request body, or uses it for something other than HTTP. A file, an ssh
|
|
162
|
+
key or an `api` header set cannot be bound — they are unpacked inside the
|
|
163
|
+
sandbox. The run trace says which way each secret went:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
secrets: GA_KEY materialised in the sandbox for bash; DATAFORSEO_PASSWORD bound to sites — filled by the egress proxy, never in the sandbox; api tools and the model key go through the egress proxy
|
|
167
|
+
```
|
|
168
|
+
|
|
115
169
|
## Connected accounts
|
|
116
170
|
|
|
117
171
|
Some third parties are connected rather than pasted — Google, GitHub,
|
package/docs/security.md
CHANGED
|
@@ -19,9 +19,14 @@ machine.
|
|
|
19
19
|
|
|
20
20
|
## What the network allows
|
|
21
21
|
|
|
22
|
-
A run pod may reach
|
|
23
|
-
|
|
24
|
-
and
|
|
22
|
+
A run pod may reach DNS, the egress proxy where its credentials are filled
|
|
23
|
+
in, and — for an agent whose [network](network) is `open` — the public
|
|
24
|
+
internet and its own account's search and browser pods; nothing else. An
|
|
25
|
+
agent that is `trusted` or `none` (the default for one that does not read
|
|
26
|
+
the web) reaches the internet only through the egress proxy, which holds
|
|
27
|
+
its list of sites per connection; on an install that enforces it
|
|
28
|
+
(`FOLDRUN_NETWORK_ENFORCE=1`) its pod has no other route, and no way to the
|
|
29
|
+
account's browser or search pods. The policy denies every private range (RFC1918) and the
|
|
25
30
|
cloud metadata endpoint — the address that, on a cloud host, hands out the
|
|
26
31
|
machine's own credentials. It cannot reach the platform's API, its
|
|
27
32
|
database, its object store, or another account's pods. Workspace files
|
|
@@ -65,10 +70,12 @@ On a cluster the value does not enter the sandbox either. The worker keeps
|
|
|
65
70
|
it and the step sends its API and model requests through a per-step
|
|
66
71
|
**egress proxy** with `${NAME}` placeholders; the proxy fills each one for
|
|
67
72
|
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
|
|
69
|
-
|
|
70
|
-
cookies), which materialises
|
|
71
|
-
|
|
73
|
+
reads its environment and finds the name, not the key. The exception is a
|
|
74
|
+
step whose script tools or Bash need the value in process (a browser seeding
|
|
75
|
+
cookies), which materialises its secrets into the sandbox and says so in the
|
|
76
|
+
trace — unless the secret is [bound to sites](secrets#bound-to-sites): a bound
|
|
77
|
+
secret never enters a sandbox, because the proxy fills it for those sites
|
|
78
|
+
whatever program sent the request.
|
|
72
79
|
|
|
73
80
|
At rest, each account has its own encryption key; the account keys are
|
|
74
81
|
wrapped by one root key the platform holds. A stolen copy of an account's
|
|
@@ -134,6 +141,20 @@ name only — for the owner and admins to read or export. Support can view an
|
|
|
134
141
|
account only [read-only, for thirty minutes, with a reason the customer
|
|
135
142
|
reads](team-and-access#support-access).
|
|
136
143
|
|
|
144
|
+
## Your terminal
|
|
145
|
+
|
|
146
|
+
The CLI prints what a platform sends it — agents' replies, logs, files,
|
|
147
|
+
release notes, errors — and a terminal obeys escape sequences hidden in
|
|
148
|
+
text: it can move the cursor and overwrite a line, clear the screen, retitle
|
|
149
|
+
the window, write your clipboard, or show a link that goes somewhere else.
|
|
150
|
+
So nothing the CLI prints carries them. The only ones that get through are
|
|
151
|
+
the CLI's own colours, and every other control character but newline and
|
|
152
|
+
tab is removed, on a terminal and through a pipe alike. Content you asked for
|
|
153
|
+
byte for byte — `foldrun source cat`, `storage cat`, `library cat`, a
|
|
154
|
+
download or CSV to stdout, `api spec` — is exact when it goes into a file or
|
|
155
|
+
another program, and filtered like everything else when it lands on a
|
|
156
|
+
screen. `--json` is exact too: JSON writes control characters as escapes.
|
|
157
|
+
|
|
137
158
|
## What is still yours to think about
|
|
138
159
|
|
|
139
160
|
- A model reading a web page reads whatever is on it. Give the agent the
|