foldrun 2.9.0 → 2.10.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 CHANGED
@@ -126,6 +126,7 @@ const HELP = `foldrun — agents are just folders
126
126
  foldrun flow rotate-hook <flow> a new webhook URL for a flow; the old one stops at once — asks first, --yes means it
127
127
  foldrun flow set <flow> --step <n|agent> key=value … the step editor: model, effort, retry, timeout, verify, when, case, else, loop, until, each, max, limits, instruction (key= clears) — diff, checked, --dry-run
128
128
  foldrun flow trigger <flow> manual|webhook|schedule the trigger picker (--schedule "<cron>", --timezone <zone>) — diff, checked, --dry-run
129
+ foldrun flow compute <flow> vcpu <n> memory <GiB> disk <GiB> what one run may hold at once across its steps (its compute:) — any of them, --clear for none
129
130
  foldrun flow filter <flow> --payload <file|-|json> would this delivery start it? its filter: and the condition that held it back (--to <ws>: the deployed flow); exit 1 when not
130
131
  foldrun flow move-step <flow> --step <n|agent> drag a card: --group <g> runs it beside group g, --after <g> gives it its own group after g (0 = first) — diff, checked, --dry-run
131
132
  foldrun flow copy-step <flow> --step <n|agent>[,<n>…] the steps' markdown on stdout, as the canvas's Copy
@@ -178,7 +179,7 @@ const HELP = `foldrun — agents are just folders
178
179
  foldrun rerun --status failed [--since 24h] [--until 2h] [--flow f] rerun every run a filter matches, from --from <n> (default 1) — lists, asks (--dry-run, --to)
179
180
  foldrun observe where the account fails, retries and spends, per workspace (--to <workspace> in full, --since <days>, --json)
180
181
  foldrun usage what the account consumed, and what it was charged week by week (--days <n>, --json)
181
- foldrun usage compute the cores and GiB its running steps may hold together and in one step, and hold now (--json)
182
+ foldrun usage compute cores, GiB and disk its running steps may hold — the account, each workspace, each live run — what they hold now, and the steps waiting for room (--json)
182
183
  foldrun schedule every flow that fires on a clock, its cron line and the next times (--to <workspace>)
183
184
  foldrun schedule tick fire whatever is due now — Settings' "Run scheduler now"
184
185
  foldrun guide write the coding-agent rules: a block in AGENTS.md and CLAUDE.md importing it (--check, --print)
@@ -198,10 +199,10 @@ const HELP = `foldrun — agents are just folders
198
199
  foldrun gallery the platform's built-in tools (web) and whether you keep your own copy — also pull (a copy for offline runs), upgrade <tool>
199
200
  foldrun identity what your cloud must trust for keyless identities: the issuer and each agent's subject (--to <workspace>, --json)
200
201
  foldrun jobs the four jobs — governance, guardrail, observability, evaluation — and what each has set (--to <workspace>, --json)
201
- foldrun account the account's defaults — also set <key> <value>, clear <key> (timezone, notify, budget, concurrency, deploy_block none|secrets|security, network_enforce on|off, owner, alerts <threshold> <value>, judge model|effort <value>; or <job>.<field>, e.g. governance.owner), pause [--reason] [--stop] / resume (the kill switch), telemetry [set <url> --header name=\${SECRET} | clear | test] (runs as OpenTelemetry traces), providers (--check) (singular; accounts lists logins)
202
+ foldrun account the account's defaults — also set <key> <value>, clear <key> (timezone, notify, budget, concurrency, compute vcpu <n> memory <GiB> disk <GiB>, deploy_block none|secrets|security, network_enforce on|off, owner, alerts <threshold> <value>, judge model|effort <value>; or <job>.<field>, e.g. governance.owner), pause [--reason] [--stop] / resume (the kill switch), telemetry [set <url> --header name=\${SECRET} | clear | test] (runs as OpenTelemetry traces), providers (--check) (singular; accounts lists logins)
202
203
  foldrun account export everything the platform holds about the account as one JSON file (owner only; --file <path>, --force)
203
204
  foldrun account model the account's own model key — set <provider> --key <api key> (--key - reads stdin; custom --base-url --format), remove
204
- foldrun workspace one deployed workspace's settings (--to) — also set <name|description|timezone|budget|notify|network_enforce|owner|alerts|judge> <value> (or <job>.<field>), clear <key>, pause [--reason] [--stop] / resume (the kill switch)
205
+ foldrun workspace one deployed workspace's settings (--to) — also set <name|description|timezone|budget|compute|notify|network_enforce|owner|alerts|judge> <value> (compute vcpu <n> memory <GiB> disk <GiB>: this workspace's pool) (or <job>.<field>), clear <key>, pause [--reason] [--stop] / resume (the kill switch)
205
206
  foldrun workspace vocabulary the names the dashboard's editor completes in a workspace: agents, flows, skills, tools, secrets, scripts, types, documents (--to, --json)
206
207
  foldrun notify test send one test notification from a workspace and say what happened (--to <workspace>)
207
208
  foldrun webhooks deliveries the notify: webhooks a workspace sent, newest first — status, attempts, last answer (--failed, --status <s>, --event <e>, --limit, --offset, --to)
@@ -335,7 +336,7 @@ Options
335
336
  --limit <n> runs, billing, history, changelog: how many rows
336
337
  --value "<text>" secrets set: skip the prompt (careful with shell history)
337
338
  --site <domain> secrets set, secrets sites: a site the secret is bound to — the egress proxy fills it for those alone (repeatable, or a,b)
338
- --clear secrets sites: unbind it
339
+ --clear secrets sites: unbind it; flow compute: no cap of its own
339
340
  --add <site> agent network: add a site to its network: list (repeatable)
340
341
  --remove <site> agent network: take a site off its network: list (repeatable)
341
342
  --out <file> api spec: where to write the OpenAPI document (default: stdout); check: where to write the JSON or SARIF report
@@ -377,7 +378,7 @@ Platform options (deploy, invoke, secrets, logs, keys)
377
378
  --local deploy: into the installation on this machine, even when signed in
378
379
  --commit <sha> deploy: record which commit this is
379
380
  --overwrite import: replace files the workspace already has with different text (refused otherwise)
380
- --dry-run deploy: check and report, change nothing; flow add, flow set, flow trigger, flow move-step, flow paste, flow draft, assistant, history restore, agent link, agent unlink: show the diff, write nothing; restore <ws>: the diff only, restore nothing; import: the preview only, write nothing; stop, rerun (bulk): list what matches, touch nothing
381
+ --dry-run deploy: check and report, change nothing; flow add, flow set, flow trigger, flow compute, flow move-step, flow paste, flow draft, assistant, history restore, agent link, agent unlink: show the diff, write nothing; restore <ws>: the diff only, restore nothing; import: the preview only, write nothing; stop, rerun (bulk): list what matches, touch nothing
381
382
  --no-runtimes deploy: do not wait for the workspace's environments to be built
382
383
  --force deploy: deploy even while runs are in flight; pull, storage get: overwrite local files
383
384
  --yes deploy: allow deleting files this folder no longer has (asked otherwise; required with no terminal)
package/docs/agents.md CHANGED
@@ -370,6 +370,10 @@ All three are **hard ceilings**:
370
370
  whole machine from the steps beside it. When every core is busy, a bigger
371
371
  class gets a bigger share.
372
372
  - **Disk:** a step over it is evicted rather than the node filling up.
373
+ This is the step's scratch space, not the account's storage: the
374
+ sandbox's copy of the workspace and whatever the step writes while it
375
+ runs. When the step ends, the files it changed are copied back to the
376
+ workspace and the sandbox, scratch space included, is deleted.
373
377
 
374
378
  Pick `small` for an agent that mostly waits on an API, `heavy` for one that
375
379
  renders a browser or holds real data. The step's `resources:` line reports CPU
package/docs/api.md CHANGED
@@ -66,7 +66,7 @@ api workspaces/$WS/flows/publish/run -X POST # start a flow
66
66
  | Route | Methods | |
67
67
  |---|---|---|
68
68
  | `/api/workspaces` | GET, POST | list; create (`name`, plus one of: `template: true` — blank, just an AGENTS.md, what the dashboard sends; `starter: true` — the example researcher, writer and publish flow; `demo: true`; `files`). A POST to a name that exists deploys over it; add `ifAbsent: true` to create only — a name that exists is then `409` and nothing changes, decided atomically (the assistant's Create workspace and `foldrun assistant` send it) |
69
- | `/api/workspaces/<ws>` | GET, PATCH, DELETE | read; rename or re-describe (`name`, `description`), or set its defaults (`timezone`, `budget`, `notify`, `network_enforce` — null clears one, edited in AGENTS.md in place); delete |
69
+ | `/api/workspaces/<ws>` | GET, PATCH, DELETE | read; rename or re-describe (`name`, `description`), or set its defaults (`timezone`, `budget`, `compute` — `{ vcpu, memGiB, diskGiB }`, this workspace's own pool, merged over what is set —, `notify`, `network_enforce`, `owner`, `alerts`, `judge` — null clears one, edited in AGENTS.md in place; keys only the account may set are ignored); delete |
70
70
  | `/api/workspaces/<ws>/pause` | GET, POST, DELETE | the workspace's kill switch. GET `{ paused, account, workspace }` — what holds runs here (`paused` is the one that applies, `scope` saying whose), each `{ at, by, reason? }` or null. POST `{ reason?, stop?: true }` pauses: every start here — schedule, webhook, chained flow, a person's Run, a rerun — is refused `423` with who paused it, and a refused trigger is logged as `halted`; A run queued before the pause and not yet begun waits until the resume. `stop: true` also stops every running, queued and parked run (needs `run:start`) and answers `stopped` and `failed`. DELETE resumes. Needs `workspace:manage`. The account's pause holds here too and only the account's DELETE lifts it; an operator's suspension is separate and neither lifts it |
71
71
  | `/api/workspaces/<ws>/vocabulary` | GET | which document kinds this workspace admits — its `docs` list is what `[[ ]]` resolves: knowledge, memory, `state/` and `storage/` files, plus the folders themselves |
72
72
  | `/api/workspaces/<ws>/check` | GET | what `foldrun check` says about the deployed copy, as findings with rule ids — what a deploy refuses or warns about, and the scanner's secret, hidden-text, agent-risk and supply-chain rules ([Checks](checks)). `?security=1` keeps the security rules, `?rule=<id or category>` (repeatable) one, `?online=1` adds the network checks (known vulnerabilities, remote MCP tool text); `{ counts, findings: [{ rule, level, file, line, message, fingerprint }], rules, online?: { problems, sources }, rescan: { checkedAt, findings, pending, problems } | null }`. Read-only, no model call |
@@ -133,7 +133,7 @@ installer's `error`, its cause on the first line. `foldrun deploy` polls it afte
133
133
  | Route | Methods | |
134
134
  |---|---|---|
135
135
  | `/api/workspaces/<ws>/flows` | GET, POST | list; create (`name`, `pattern`) |
136
- | `/api/workspaces/<ws>/flows/<flow>` | POST, PATCH, DELETE | edit a step (`step`, `instruction`, `options`, `target`, `subflow`), rearrange (`groups`), set `trigger`, a canvas edit (`edit`, `edits`), undo (`restore` + `expect`); delete. Every PATCH and DELETE takes `expect` — the text the step numbers were read from, or its sha256 — and is refused with `409` and the current `text`, nothing written, when the file no longer reads it (the canvas, the list view's board and step options, and the CLI's `flow add`/`rm-step`/`dup-step --to` send it; the dashboard shows the 409 as "this flow changed since you opened it — reload") |
136
+ | `/api/workspaces/<ws>/flows/<flow>` | POST, PATCH, DELETE | edit a step (`step`, `instruction`, `options`, `target`, `subflow`), rearrange (`groups`), set `trigger`, set `compute` (`{ vcpu, memGiB, diskGiB }` or `null` — what one run may hold at once, merged over what the flow says), a canvas edit (`edit`, `edits`), undo (`restore` + `expect`); delete. Every PATCH and DELETE takes `expect` — the text the step numbers were read from, or its sha256 — and is refused with `409` and the current `text`, nothing written, when the file no longer reads it (the canvas, the list view's board and step options, and the CLI's `flow add`/`rm-step`/`dup-step --to` send it; the dashboard shows the 409 as "this flow changed since you opened it — reload") |
137
137
  | `/api/workspaces/<ws>/flows/draft` | POST, PATCH, DELETE | POST draft a flow with a model: `{ description, flow? }` (`flow` redrafts that one). Returns `{ ok, files: [{ path, content, before, sha256 }], issues: [{ where, message, level }], repaired, notes }` — checked with the deploy's checks and the flow lint, repaired once. **Writes nothing**; save with the source PUT. Needs `workspace:write`; a description over 4,000 characters is `413`; a Chat-Completions `provider:` is `422`. On the platform's models the balance is checked first (`402`) and the call is charged as one ledger line, flow `ai-draft`. PATCH and DELETE, and a POST without `description`, are the edits of a flow that happens to be named `draft`, exactly as at `flows/<flow>` |
138
138
  | `/api/workspaces/<ws>/flows/<flow>/run` | POST | start it. An `Idempotency-Key` header, or `idempotencyKey` in the body, makes a retried call safe: a dropped response, a proxy timeout or a CI step that ran twice is answered `started: false` with the `runId` it already started, rather than starting a second run and billing for it. Keys are remembered for 24 hours, per flow |
139
139
  | `/api/workspaces/<ws>/flows/<flow>/filter` | POST | test the flow's `filter:` against a delivery without starting anything: `{ payload }` — a webhook's JSON body (or the body as a string, read as JSON), an email's `{ from, to, subject, text }`, a chained run's `{ run, flow, status, ending, verdict, summary }`, a storage event's `{ by, paths }`. Answers `{ flow, trigger, filter, applies, match, reason }` — `reason` names the first condition that failed; a flow with no filter matches everything. Reads the deployed flow; nothing is recorded or charged |
@@ -340,7 +340,7 @@ Reads return which secrets exist, never their values.
340
340
  | `/api/team` | GET, POST | members with their roles and workspace scope; mint an invite — `role` (viewer \| editor \| admin, default editor), `workspaces` (names, or omit for all), `email` (bound into the invite: only that address can accept it; sent through the account's `RESEND_API_KEY` unless `send: false`). Returns `path` (the invite's relative URL), `expires`, `sent` and any `sendError` either way; the full link is in the mail |
341
341
  | `/api/team/members/<id>` | PATCH, DELETE | change a member's role (`role`) and/or which workspaces they may open (`workspaces`: names, or null for all); remove them |
342
342
  | `/api/team/transfer` | POST | hand the account to a member (`to`) — owner only; you become an admin |
343
- | `/api/usage/compute` | GET | the account's compute: `compute` is `{ vcpu, memGiB, stepVcpu, stepMemGiB, source }` — the cores and GiB all its running steps may hold together, the most one step may hold (each `null` for no cap), and where the pool came from (`plan`, `account` — its own `compute:` in AGENTS.md, which can only lower it — `install`, or `null`); `inUse` is `{ vcpu, memGiB, steps }`, what its running steps' sandboxes hold now, from their records (a step still waiting for room holds nothing; a caller scoped to some workspaces sees theirs). A step past the pool waits for one of the account's steps to finish; one bigger than the pool or the step maximum fails at once ([agents](agents#resources-your-own-numbers)) |
343
+ | `/api/usage/compute` | GET | the account's compute at every level (27 Oct 2026): `compute` is `{ vcpu, memGiB, diskGiB, stepVcpu, stepMemGiB, stepDiskGiB, source }` — the cores, GiB and GiB of disk all its running steps may hold together, the most one step may hold (each `null` for no cap), and where the pool came from (`plan`, `account` — its own `compute:` in AGENTS.md, which can only lower it — `install`, or `null`); `inUse` is `{ vcpu, memGiB, diskGiB, steps }`, what its running steps' sandboxes hold now, from their records (a step still waiting for room holds nothing; a caller scoped to some workspaces sees theirs); `workspaces` is each workspace's own `pool` (its AGENTS.md `compute:`, `null` figures for none) and `inUse`; `runs` is each live run with a pool of its own (its flow's `compute:`) or anything held, `{ workspace, runId, flow, pool, inUse }`; `waiting` lists the steps waiting for room, oldest first — `{ workspace, runId, flow, step, agent, since, why, cpus, memGiB, diskGiB, level?, runPool }`, `since` being when it first found none, its place in line, and `level` the cap that held it when it last looked. A step past any level waits, with no time limit, and starts in turn; one bigger than a level's whole cap or the step maximum fails at once ([four levels](budgets-and-billing#compute-at-four-levels), [waiting for room](budgets-and-billing#waiting-for-room)) |
344
344
  | `/api/usage/history?days=` | GET | charged spend by flow, by agent and by workspace over time, read from the ledger rather than the run records — so it survives runs being pruned and reports what was billed rather than what was consumed. Buckets by week over three weeks, by day under. Each series carries its periods, its total, and the change between the last two COMPLETE periods, so a partial week never reads as a collapse. `unattributedUsd` is charges from before a line recorded its flow. A caller scoped to some workspaces sees only the charges in those; `days` 1–365, default 56 |
345
345
  | `/api/account/model` | GET, PUT, DELETE | the account's own model key — what every step runs on unless an agent, workspace or the account's AGENTS.md names a `provider:`. GET: `{ set, provider, baseUrl, format, keyHint, runsOn, platformKeyAllowed }` — never the key, only its last four characters; `runsOn` is `account` (its own key), `platform` (the platform's credential, for the operator's accounts) or `none` (runs are refused until a key is set). PUT `{ provider, apiKey, baseUrl?, format? }` (needs `secrets:write`): `provider` is a preset name (anthropic, openrouter, openai, groq, … — see providers) or `custom` with an https `baseUrl` and a `format` (anthropic or openai); a Claude login token (sk-ant-oat…) is refused for any account not allowed the platform's credential. Stored as the account secrets `FOLDRUN_MODEL_PROVIDER`, `FOLDRUN_MODEL_API_KEY`, `FOLDRUN_MODEL_BASE_URL`, `FOLDRUN_MODEL_FORMAT`. DELETE removes them. Honours Idempotency-Key on PUT; audited as model.key_set / model.key_removed |
346
346
  | `/api/account/providers` | GET, POST | GET: the model providers this account's files actually use — each one's preset name, where it is declared, the model a check would send — plus the last daily health check. POST forces one now, which spends one minimal request at each distinct endpoint, and needs the permission that runs |
@@ -359,7 +359,7 @@ Reads return which secrets exist, never their values.
359
359
  | `/api/account/api` | GET, PATCH | how this account meets the API — GET `{ version: { current, pinned, effective, supported, supportMonths }, limits: { plan, windowSeconds, perCredential: { read, write }, perAccount: { read, write } }, usage }`. `usage` is the last 30 days — `{ days, versions: [{ version, requests, lastSeen }], deprecated: [{ route, requests, lastSeen, deprecatedAt, sunsetAt, link, callers: [{ credential, requests, lastSeen }] }] }`, `credential` naming a key by its label and prefix — or `null` when it could not be read; PATCH `{ version: "YYYY-MM-DD" \| null }` pins the version API-key requests without a `Foldrun-Version` header get (null: always current); the dashboard always gets current. `keys:manage`. See [api-usage](api-usage.md) |
360
360
  | `/api/account/flags` | GET | the feature flags as they stand for this account — `{ flags: [{ name, description, owner, default, on, source, rollout }] }`, `source` being what decided it: `account` (an override for this account), `global` (the install-wide setting), `rollout` (a percentage of accounts, by a stable hash of account and flag; `rollout` is the percentage) or `default`. Read-only, every role; only the super admin changes a flag. See [Feature flags](feature-flags.md) |
361
361
  | `/api/openapi.json` | GET | this API as an OpenAPI 3.1 document — every route and method, bearer auth, the rate-limit, version and Idempotency-Key headers, and request/response schemas for runs, flow runs, deploy, source, secrets, keys and billing. No key needed. Generated from the route files by `scripts/gen-openapi.mjs`; the dashboard renders it at Docs → API reference, `foldrun api spec` saves it |
362
- | `/api/account` | GET, PATCH | the account's defaults from its AGENTS.md frontmatter — `timezone`, `budget` (a USD cap — a number for a month, `"60/day"`, `"60/week"`, or `{ usd, period }`; null is no limit), `notify` (`{ url?, email?, events }`), `concurrency`, `compute` (`{ vcpu, memGiB }` — the account's own cap on what its running steps hold together, lower than the plan's; PATCH writes it as `compute: { vcpu, memory }`), `deploy_block` (`none`, `secrets` or `security` — answered as `deployBlock`), `network_enforce` (answered as `networkEnforce`), `owner`, `alerts` (as written), `judge` (`{ model?, effort? }`), `telemetry` (`{ otlp, headers }` — header values are `${SECRET}` names; answered as `{ endpoint, headers }`); PATCH sets the keys sent, null clears one, and edits the file in place |
362
+ | `/api/account` | GET, PATCH | the account's defaults from its AGENTS.md frontmatter — `timezone`, `budget` (a USD cap — a number for a month, `"60/day"`, `"60/week"`, or `{ usd, period }`; null is no limit), `notify` (`{ url?, email?, events }`), `concurrency`, `compute` (`{ vcpu, memGiB, diskGiB }` — the account's own cap on what its running steps hold together, lower than the plan's; PATCH writes it as `compute: { vcpu, memory, disk }`, merged over what is set: a figure not sent is kept, `null` drops one, `compute: null` drops all), `deploy_block` (`none`, `secrets` or `security` — answered as `deployBlock`), `network_enforce` (answered as `networkEnforce`), `owner`, `alerts` (as written), `judge` (`{ model?, effort? }`), `telemetry` (`{ otlp, headers }` — header values are `${SECRET}` names; answered as `{ endpoint, headers }`); PATCH sets the keys sent, null clears one, and edits the file in place |
363
363
  | `/api/audit?since&until&action&actor&workspace&limit&cursor&format` | GET | the account's audit log, newest first — the caller's account only, whatever a header, query or body names. The owner and unscoped admins (people, or `admin` keys); `audit:read`, and refused to a member or key scoped to some workspaces, because the log is the whole account's. `since`/`until`: `7d`, `24h`, `30m` or a date; `action`: exact (`apikey.created`) or a family (`apikey`); `actor`: exact (an email, `key:<label>`, `support:<email>`); `workspace`: entries about one; `limit` 1–1000 (100; 1000 for CSV); `cursor`: the `next` of the page before. JSON `{ entries: [{ id, at, actor, action, subject, detail }], next, retention }`; `format=csv` downloads the same rows (`x-next-cursor` header when there are more). Records sign-ins and refusals, sign-outs, session revokes, password and email changes, MFA on/off, key mint/rotate/revoke, invites, joins, role/scope changes, removals, ownership transfers, secrets set/deleted (by name — never a value), deploys, workspace deletes, plan and billing changes, export and close requests, and `support.viewed`/`support.ended`. The super admin's own levers (`operator.*`) are the platform's record and not listed. Kept for the life of the account |
364
364
  | `/api/avatars/<kind>/<id>` | GET, PUT, DELETE | a picture for a `user` (their own id), a `workspace` (its name) or the `account` (the tenant). PUT the bytes of a PNG, JPEG or WebP under 1 MB; a person sets their own, an admin a workspace's or the account's. GET needs a signed-in tenant and is privately cached for a year — the URL's `?v=` changes with the picture |
365
365
  | `/api/search?q=` | GET | find anything in the account by name — workspaces, agents, flows, tools, skills, knowledge, memory, runs (by id, flow or headline); ranked, at most six per kind. The dashboard's Find palette reads it |
@@ -428,7 +428,7 @@ X-Foldrun-Cli: 0.6.0 # the CLI released with this version (absent
428
428
 
429
429
  | Route | Methods | |
430
430
  |---|---|---|
431
- | `/api/auth/signup` | POST | `email`, `password`, `account`, `invite`. Rate limited per address and per caller. With signups closed every address gets the same refusal, so the form cannot be used to discover which addresses are registered |
431
+ | `/api/auth/signup` | POST | `email`, `password`, `account`, `invite`, `setup`. An install with `FOLDRUN_SETUP_TOKEN` set creates its first account only with `setup` equal to it (403 otherwise); once an account exists the token does nothing. Rate limited per address and per caller. With signups closed every address gets the same refusal, so the form cannot be used to discover which addresses are registered |
432
432
  | `/api/auth/login` | POST | `email`, `password` |
433
433
  | `/api/auth/logout` | POST | ends the session |
434
434
  | `/api/auth/session` | GET | who am I |
@@ -446,8 +446,9 @@ X-Foldrun-Cli: 0.6.0 # the CLI released with this version (absent
446
446
  | `/api/auth/mfa/disable` | POST | `password` and `code` — a session alone is not enough to remove the second factor |
447
447
  | `/api/auth/timezone` | POST | `timezone` — what a cron expression means |
448
448
 
449
- Login is rate limited per IP, which only works when `FOLDRUN_TRUST_PROXY=1`
450
- tells the platform a real proxy is reporting the caller.
449
+ Login is rate limited per IP, which only works when `FOLDRUN_TRUST_PROXY`
450
+ (`1`, or `cloudflare` with `FOLDRUN_EDGE_SECRET`) tells the platform a real
451
+ proxy is reporting the caller.
451
452
 
452
453
  ### Signing the CLI in
453
454
 
@@ -27,8 +27,8 @@ wins, and the step's error says which line set it: `budget: in the flow
27
27
  file` or `budget: on the writer agent`.
28
28
  | **runs at once** | `concurrency: 3` in the account's `AGENTS.md`, or Settings → Governance | the fourth run waits its turn in the queue — never refused, only later. Empty means the install's share (`FOLDRUN_ACCOUNT_CONCURRENCY`) or plan number (`FOLDRUN_PLAN_CONCURRENCY`, which applies only where billing is on), or no cap. Your own number can lower that, never raise it; a higher number comes with a plan |
29
29
  | **the queue lane** | the plan's lane (`high` \| `normal` \| `low`); `priority: low` in the account's `AGENTS.md` lowers it, and `high` there counts only with a plan that gives it | when slots are short, accounts take turns, weighted by lane — high 3, normal 2, low 1: every account's first run goes before anyone's second, a fast lane takes three turns to a slow lane's one and never all of them, an account with runs going waits behind one with none, and a run that has waited ten minutes moves up a turn. A flow's own `priority:` orders runs within that. The cap above still holds |
30
- | **compute at once** | the plan's cores and GiB (below); `compute: { vcpu: 4, memory: 8Gi }` in the account's `AGENTS.md`, or Settings → Governance, lowers it | what all the account's running steps may hold together — each step's cores and memory, its `size:` class's or its own `resources:` ([agents](agents#resources-your-own-numbers)). A step that would pass it waits to start, like a full node — `room: waiting to start — 6 of this account's 8 GiB is held by its running steps…` — and starts when one of the account's steps ends. A step that **alone** is bigger than the pool, or than the plan's most for one step, fails at once with that message: no step finishing would make room. Without a plan, the install's `FOLDRUN_ACCOUNT_VCPU` / `FOLDRUN_ACCOUNT_MEMORY_GIB`, or no cap. Your own number can lower it, never raise it. `foldrun usage compute` shows the pool and what is held now. Enforced on the hosted platform and any k8s install; a self-hosted Docker install caps each step at its class but not the pool |
31
- | **room to start** | the install's `FOLDRUN_RUN_MEMORY_GIB` and `FOLDRUN_ACCOUNT_PODS` | a step whose sandbox would not fit, or whose account already has its most sandboxes running, waits to start — `room: waiting to start — …` on its trail — instead of failing. The wait is not billed; the run page's start-up chip shows it as `room`. A step still waiting after 30 minutes (`FOLDRUN_ROOM_WAIT_SEC`) fails, saying why |
30
+ | **compute at once** | the plan's cores, GiB and disk (below); `compute: { vcpu: 4, memory: 8Gi, disk: 40Gi }` in the account's `AGENTS.md`, or Settings → Governance, lowers it — and a workspace's and a flow's `compute:` narrow it further ([four levels](#compute-at-four-levels)) | what all the account's running steps may hold together — each step's cores, memory and disk, its `size:` class's or its own `resources:` ([agents](agents#resources-your-own-numbers)). A step that would pass it waits to start, like a full node — `room: waiting to start — 6 of this account's 8 GiB is held by its running steps…` — and starts in turn when one of the account's steps ends ([waiting for room](#waiting-for-room)). A step that **alone** is bigger than the pool, or than the plan's most for one step, fails at once with that message: no step finishing would make room. Without a plan, the install's `FOLDRUN_ACCOUNT_VCPU` / `FOLDRUN_ACCOUNT_MEMORY_GIB` / `FOLDRUN_ACCOUNT_DISK_GIB`, or no cap. Your own number can lower it, never raise it. `foldrun usage compute` shows the pool and what is held now. Enforced on the hosted platform and any k8s install; a self-hosted Docker install caps each step at its class but not the pool |
31
+ | **room to start** | the install's `FOLDRUN_RUN_MEMORY_GIB`, `FOLDRUN_RUN_DISK_GIB` and `FOLDRUN_ACCOUNT_PODS` | a step whose sandbox would not fit, or whose account already has its most sandboxes running, waits to start — `room: waiting to start — …` on its trail — instead of failing. The wait is not billed and has no time limit; the run page's start-up chip shows it as `room` ([waiting for room](#waiting-for-room)) |
32
32
 
33
33
  A cap is a literal, never an expression, so it is readable off the file. The
34
34
  run cap is hard: a step that reaches its share of the remainder is stopped
@@ -58,6 +58,73 @@ work never trips it, a runaway is stopped where it went wrong, and the
58
58
  failure notification says why. The dashboard's Usage page shows spend by
59
59
  workspace and by meter.
60
60
 
61
+ ### Compute at four levels
62
+
63
+ CPU, memory and disk are each capped at four levels (27 Oct 2026). Every
64
+ level only narrows the one above it; a step must fit all of them.
65
+
66
+ | level | where | what it caps |
67
+ |---|---|---|
68
+ | **account** | the plan (or the install's `FOLDRUN_ACCOUNT_*`); `compute:` in the account's `AGENTS.md`, Settings → Governance or `foldrun account set compute` lowers it | everything the account's running steps hold together |
69
+ | **workspace** | `compute:` in the workspace's `AGENTS.md`, its Settings → Governance, or `foldrun workspace set compute … --to <ws>` | what that workspace's running steps hold together |
70
+ | **run** | `compute:` in the flow, the flow's card, or `foldrun flow compute <flow> …` | what one run of the flow holds at once across its parallel steps — each run has its own |
71
+ | **step** | the agent's `size:` or `resources: { cpu, memory, disk }`; the plan's (or install's `FOLDRUN_STEP_MAX_*`) most for one step | one step's sandbox |
72
+
73
+ ```yaml
74
+ # AGENTS.md (the account's or a workspace's), or a flow's frontmatter
75
+ compute: { vcpu: 4, memory: 8Gi, disk: 40Gi }
76
+ ```
77
+
78
+ Any of the three may be left out. A step that would pass a cap waits, in
79
+ turn, and its trail says which level holds it — `room: waiting to start —
80
+ 3 of this run's 4 vCPU is held by its running steps…`. A step that **alone**
81
+ is bigger than a level's whole cap fails at once, naming it. A workspace's
82
+ figure above the account's grants nothing: the account's still applies.
83
+ `foldrun usage compute` and `GET /api/usage/compute` show every level — the
84
+ account, each workspace and each live run — with what it holds. Before
85
+ 27 Oct 2026 a workspace's `compute:` was read nowhere; it now caps that
86
+ workspace.
87
+
88
+ ### Waiting for room
89
+
90
+ A step that finds no room — its run's, workspace's or account's pool, its
91
+ account's most sandboxes at once, or the node's memory or disk —
92
+ **waits; a wait never fails a step**. There
93
+ is no time limit (until 27 Oct 2026 a step failed after 30 minutes). Only a
94
+ step that alone is bigger than the pool or the plan's most for one step
95
+ fails, at once, because nothing finishing would make room.
96
+
97
+ - **In turn.** Within an account the oldest waiting step starts first: a
98
+ later, smaller step never takes the gap an older, bigger one is waiting
99
+ for. Across accounts, the node's last room is kept for the oldest step
100
+ whose own account would let it start. A step's place in line is when it
101
+ first found no room, and it keeps it.
102
+ - **In the queue, not on a worker.** After a minute of waiting
103
+ (`FOLDRUN_ROOM_PARK_AFTER_SEC`), once nothing else of its group is
104
+ running, the run goes back to the queue with its place and looks again
105
+ every 20 seconds (`FOLDRUN_ROOM_RECHECK_SEC`), so waiting runs never hold
106
+ the workers other accounts' runs need. A step whose sibling is still
107
+ running waits beside it. A wait is not a try: it spends none of the
108
+ step's `retry:`, and a stop ends it like any other.
109
+ - **Seen.** The run page says *Waiting for room to start*, why, and since
110
+ when; Settings → Governance and `foldrun usage compute` list every
111
+ waiting step, oldest first (`waiting` in `GET /api/usage/compute`).
112
+ - **Told.** After an hour (`FOLDRUN_ROOM_ALERT_MIN`) the workspace's
113
+ `notify:` hears of it once, as event `room`, with what to change: ask for
114
+ less (`size:`, `resources:`) or allow the account more compute.
115
+
116
+ - **In turn, by scope.** An older step goes first among the steps of the
117
+ scope that holds it back: its run's own pool among that run's steps, its
118
+ workspace's among that workspace's, otherwise among all its account's —
119
+ so a step stuck behind a narrow pool of its own never holds back the rest.
120
+
121
+ Nothing is charged while a step waits — the meter starts when its sandbox
122
+ does. A sandbox the cluster has no node with room for yet — not enough cpu,
123
+ memory or disk free (it says so: *no node has room for this sandbox yet*)
124
+ — waits too, and that time is credited off the bill; one it can never
125
+ place (a claim that does not exist, no node that matches) still fails. On a self-hosted Docker install there is no room gate, so nothing
126
+ waits.
127
+
61
128
  ## Plans and credits
62
129
 
63
130
  Usage is sold in **credits: one credit is one cent** of what a step consumes
@@ -67,16 +134,18 @@ the money is shown and bought.
67
134
 
68
135
  | plan | a month | credits a month | an extra credit | runs at once | compute at once | one step, at most | workspaces | queue lane |
69
136
  |---|---|---|---|---|---|---|---|---|
70
- | **Starter** | $29 | 2,900 | 1.00¢ | 1 | 4 vCPU · 8 GiB | 4 vCPU · 8 GiB | 3 | standard |
71
- | **Creator** | $79 | 8,800 | 0.90¢ | 2 | 8 vCPU · 16 GiB | 4 vCPU · 8 GiB | 10 | standard |
72
- | **Pro** | $249 | 29,000 | 0.86¢ | 4 | 16 vCPU · 32 GiB | 8 vCPU · 16 GiB | unlimited | fast |
73
- | **Scale** | $799 | 100,000 | 0.80¢ | 8 | 32 vCPU · 64 GiB | 8 vCPU · 16 GiB | unlimited | fast |
137
+ | **Starter** | $29 | 2,900 | 1.00¢ | 1 | 4 vCPU · 8 GiB · 40 GiB disk | 4 vCPU · 8 GiB · 20 GiB disk | 3 | standard |
138
+ | **Creator** | $79 | 8,800 | 0.90¢ | 2 | 8 vCPU · 16 GiB · 80 GiB disk | 4 vCPU · 8 GiB · 20 GiB disk | 10 | standard |
139
+ | **Pro** | $249 | 29,000 | 0.86¢ | 4 | 16 vCPU · 32 GiB · 160 GiB disk | 8 vCPU · 16 GiB · 40 GiB disk | unlimited | fast |
140
+ | **Scale** | $799 | 100,000 | 0.80¢ | 8 | 32 vCPU · 64 GiB · 320 GiB disk | 8 vCPU · 16 GiB · 40 GiB disk | unlimited | fast |
74
141
 
75
142
  **Compute at once** is what all of an account's running steps may hold
76
- together, counted by the cores and memory each step's sandbox is capped at.
77
- **One step, at most** is the largest single step the plan runs: `heavy`
78
- (4 vCPU · 8 GiB) on every plan, and up to 8 vCPU · 16 GiB with `resources:`
79
- on Pro and Scale.
143
+ together, counted by the cores, memory and disk each step's sandbox is
144
+ capped at (disk is what a step may write: its `size:` class's — 2, 8 or 20
145
+ GiB — or its own `resources: { disk }`). **One step, at most** is the largest
146
+ single step the plan runs: `heavy` (4 vCPU · 8 GiB · 20 GiB disk) on every
147
+ plan, and up to 8 vCPU · 16 GiB · 40 GiB disk with `resources:` on Pro and
148
+ Scale.
80
149
 
81
150
  **How long a credit lasts.** A plan's credits land when its invoice is paid,
82
151
  and what is unused **rolls over** into the next cycle up to **twice** the
@@ -141,7 +210,7 @@ provider's own invoice.
141
210
  | meter | what it counts |
142
211
  |---|---|
143
212
  | **tokens** | model usage at cost, times a margin. **Bring your own key and the margin does not apply** — a BYOK step is billed nothing for tokens |
144
- | **compute** | sandbox seconds: what a step reserved (cores and memory, while it ran) and what it actually burned |
213
+ | **compute** | sandbox seconds: what a step reserved (cores, memory and disk, while it ran) and what it actually burned. Disk is billed by the GiB a step is allowed to write, per second, like memory (since 27 Oct 2026) |
145
214
  | **storage and network** | GB-months in the file store, accrued daily; GB the sandbox moved, both directions |
146
215
 
147
216
  There is deliberately **no per-step fee**. Steps are sandbox-seconds, so a
package/docs/cli.md CHANGED
@@ -58,6 +58,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
58
58
  | `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 |
59
59
  | `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` |
60
60
  | `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 }` |
61
+ | `foldrun flow compute <flow> vcpu <n> memory <GiB> disk <GiB>` | what one run of the flow may hold at once across its parallel steps — its `compute:` — any of the three, merged over what is set; `--clear` for no cap of its own. With `--to`, the flows route's `{ compute }` (the flow card's control); without, the flow file's frontmatter ([four levels](budgets-and-billing#compute-at-four-levels)) |
61
62
  | `foldrun flow filter <flow> --payload <file\|-\|json>` | would this delivery start the flow? Its `filter:` against a payload — a webhook's JSON body, an email's `{ from, to, subject, text }`, a chained run's `{ run, flow, status, ending, verdict, summary }`, a storage event's `{ by, paths }` — and the condition that held it back. Reads the local flow; `--to <workspace>` asks the deployed one (`POST …/flows/<flow>/filter`, the Triggers page's Test box). Starts nothing; exits `0` when it would start a run, `1` when not; `--json` for the whole answer |
62
63
  | `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 }` |
63
64
  | `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 |
@@ -146,6 +147,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
146
147
  | `foldrun triggers` | why nothing ran. One row per flow that fired in the window: its trigger, how many fires became runs, when it last ran, and every reason for the difference with a count — a duplicate delivery, a throttled or debounced burst, a fire the platform slept through, an `overlap: skip`, a flow switched off by `disable_after:`. A switched-off flow is a non-zero exit; one successful run clears it. `--since <days>` (default 7), `--to <workspace>` |
147
148
  | `foldrun observe` | where the account fails, retries and spends: one row per workspace — runs, failed and what share, steps, cost — then the recent failures in their own words. `--to <workspace>` prints that workspace in full, as the Observe page does: per flow (runs, failed, p50/p95 duration, cost), per agent (failed, retried, skipped, p95, turns, cache hit rate), per tool (calls, errors, p95 latency, result size p50/p95 and how many results were over 10k characters, which agents use it), and start-up — each phase of a step's first seconds, its own share and when it was done, p50/p95. `--since <days>` (default 30), `--json` for the document |
148
149
  | `foldrun usage` | what the account consumed — runs, steps, tokens (with the share the prompt cache served), compute, storage, per workspace — and what it was charged over `--days <n>` (default 56) from the ledger, per workspace with the change against the period before. `--json` for both documents |
150
+ | `foldrun usage compute` | the cores, GiB and disk the account's running steps may hold together and in one step, at every level — the account, each workspace with a pool or anything held, each live run with its flow's pool — what each holds now, and the steps **waiting for room**, oldest first, each with its flow, agent, what it needs, how long it has waited, why and which level holds it (`GET /api/usage/compute`). A waiting step has no time limit and starts in turn; `--json` for the document. Set the levels with `foldrun account set compute …`, `foldrun workspace set compute … --to <ws>` and `foldrun flow compute <flow> …` |
149
151
  | `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 |
150
152
  | `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`) |
151
153
  | `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` |
@@ -153,8 +155,8 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
153
155
  | `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 |
154
156
  | `foldrun identity` | keyless cloud identity: the issuer your cloud must trust and the subject each agent's tokens carry (`GET /api/identity`); `--to <workspace>` fills the workspace in, `--json` |
155
157
  | `foldrun jobs` | the four jobs — governance, guardrail, observability, evaluation — and every `AGENTS.md` field of each, with what is set (`GET /api/account/jobs`). `--to <workspace>` shows the workspace's value beside the account's and how the two combine; `--json` the document. `account set <job>.<field>` and `workspace set <job>.<field>` refuse a field that scope cannot set and name the `AGENTS.md` to write it in. See [The four jobs](four-jobs) |
156
- | `foldrun account` | the account's own defaults — the AGENTS.md frontmatter every workspace under it inherits: `timezone`, `notify`, `budget`, `concurrency`, `deploy_block`, `network_enforce`. `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`, `set deploy_block secrets` (`none`, `secrets` or `security` — what the scanner's findings do to a deploy), `set network_enforce on` (every workspace refuses a site off an agent's `network:` list; `off` reports only), `set owner <email or name>`, `set alerts <threshold> <value>` (merged into what is set; `off` removes one), `set judge model|effort <value>`. Any setting may be named by its job — `set governance.deploy_block secrets`, `set observability.alerts stale_days 30` — and one named under the wrong job is refused with the right one, `pause` (`--reason "…"`, `--stop` to also stop every run going now — asks, or `--yes`) and `resume`: the kill switch, no new run starts in any workspace while paused, `telemetry` — where each finished run goes as an OpenTelemetry trace: bare shows it, `set <url> --header authorization='${OTEL_TOKEN}'` (repeatable; a value is a vault secret's name), `clear`, `test` (one made-up run, and what the collector answered; exit 1 when not delivered), 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 |
157
- | `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 network_enforce on` (refuse a site off an agent's `network:` list here; `off` or `clear` goes back to the account's), `set owner`, `set alerts <threshold> <value>`, `set judge model|effort <value>`, and `<job>.<field>` as on `account`, `pause` (`--reason`, `--stop`) and `resume` — this workspace's kill switch, `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 |
158
+ | `foldrun account` | the account's own defaults — the AGENTS.md frontmatter every workspace under it inherits: `timezone`, `notify`, `budget`, `concurrency`, `deploy_block`, `network_enforce`. `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`, `set compute vcpu 4 memory 8 disk 40` (GiB; any of them, merged over what is set — the account's own pool, lower than the plan's), `set deploy_block secrets` (`none`, `secrets` or `security` — what the scanner's findings do to a deploy), `set network_enforce on` (every workspace refuses a site off an agent's `network:` list; `off` reports only), `set owner <email or name>`, `set alerts <threshold> <value>` (merged into what is set; `off` removes one), `set judge model|effort <value>`. Any setting may be named by its job — `set governance.deploy_block secrets`, `set observability.alerts stale_days 30` — and one named under the wrong job is refused with the right one, `pause` (`--reason "…"`, `--stop` to also stop every run going now — asks, or `--yes`) and `resume`: the kill switch, no new run starts in any workspace while paused, `telemetry` — where each finished run goes as an OpenTelemetry trace: bare shows it, `set <url> --header authorization='${OTEL_TOKEN}'` (repeatable; a value is a vault secret's name), `clear`, `test` (one made-up run, and what the collector answered; exit 1 when not delivered), 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 |
159
+ | `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 compute vcpu 2 memory 4 disk 20` (this workspace's own pool, inside the account's; any of them), `set notify email <address>` / `set notify url <https://…>` (`--events`; merged with what the file says), `set network_enforce on` (refuse a site off an agent's `network:` list here; `off` or `clear` goes back to the account's), `set owner`, `set alerts <threshold> <value>`, `set judge model|effort <value>`, and `<job>.<field>` as on `account`, `pause` (`--reason`, `--stop`) and `resume` — this workspace's kill switch, `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 |
158
160
  | `foldrun workspace vocabulary` | what the dashboard's editor completes as you type in a workspace (`--to`): agents, flows, skills, tools, secrets (names only), scripts, document types and linkable documents (`GET …/vocabulary`; `--json`) |
159
161
  | `foldrun notify test` | send one test notification to whatever the workspace's `notify:` resolves to, and print what happened — the provider's own words when it failed ("domain not verified"). Exits 1 when it did not go. `--to <workspace>` |
160
162
  | `foldrun webhooks deliveries` | the `notify:` webhooks a workspace sent, newest first: event, status (`delivered`, `retrying`, `failed`, `skipped`), attempts, the last answer and the delivery id — and a switched-off endpoint first, with the way back. `--failed` for the ones that gave up, `--status <s>`, `--event <e>`, `--limit`, `--offset`, `--json`. `--to <workspace>` |
package/docs/flows.md CHANGED
@@ -332,6 +332,7 @@ normal outcome, recorded as one.
332
332
  | `filter` | for a `webhook`, `email`, `flow` or `storage` trigger: the fields a delivery must have to start a run — see [Which deliveries start a run](#which-deliveries-start-a-run) |
333
333
  | `model` / `effort` | defaults for every step |
334
334
  | `budget` | the most one run may spend, in USD — a number, or `unlimited`; unset is no limit |
335
+ | `compute` | what one run's steps may hold together at once — `{ vcpu: 2, memory: 4Gi, disk: 20Gi }`, any of them. Each run has its own; it only narrows the workspace's and account's. A step past it waits for one of its run's steps to finish; one bigger than it fails at once ([four levels](budgets-and-billing#compute-at-four-levels)) |
335
336
  | `overlap` | `skip` or `queue` when a run is already live |
336
337
  | `issues` | `stay-open` (the default: a person resolves this flow's [issues](runs#issues)) or `auto-close` (a run that goes fine closes them) |
337
338
  | `priority` | `high`, `normal` or `low` — where this flow's runs stand in the queue against everyone else's |
@@ -73,8 +73,8 @@ summary leads with none.
73
73
 
74
74
  ## The three that are about nothing happening
75
75
 
76
- `failed`, `completed`, `blocked` and `awaiting-approval` are all about a run. Three
77
- more events are about a run that **did not** happen, or has not finished,
76
+ `failed`, `completed`, `blocked` and `awaiting-approval` are all about a run. The
77
+ events below are about a run that **did not** happen, or has not finished,
78
78
  which is the class of failure that otherwise reaches nobody:
79
79
 
80
80
  | event | sent when |
@@ -83,6 +83,7 @@ which is the class of failure that otherwise reaches nobody:
83
83
  | `sla` | a run has passed its flow's `sla:` and is still going |
84
84
  | `budget` | a workspace has spent 80%, then 95%, of its `budget:` for the window — today, this week or this month |
85
85
  | `security` | the daily rescan found a security finding that was not there before — see [Checks](checks#online-checks-and-the-daily-rescan) |
86
+ | `room` | a run has waited an hour for room to start (`FOLDRUN_ROOM_ALERT_MIN`) — it keeps its place and starts when there is room; see [waiting for room](budgets-and-billing#waiting-for-room) |
86
87
 
87
88
  Each is sent once per occurrence, not once per check: an alarm that repeats
88
89
  every thirty seconds gets muted, and a muted alarm is the same as no alarm.
@@ -230,7 +231,7 @@ Every attempt carries these headers:
230
231
  | header | |
231
232
  |---|---|
232
233
  | `X-Foldrun-Delivery` | the delivery's id, `dlv_…` — **the same on every attempt and every redelivery**. Dedupe on it: a receiver that took attempt 2 but timed out answering may see attempt 3 |
233
- | `X-Foldrun-Event` | `failed`, `completed`, `blocked`, `awaiting-approval`, `quarantined`, `sla`, `budget`, or `test` |
234
+ | `X-Foldrun-Event` | `failed`, `completed`, `blocked`, `awaiting-approval`, `quarantined`, `sla`, `budget`, `room`, or `test` |
234
235
  | `X-Foldrun-Attempt` | 1, 2, … |
235
236
  | `X-Foldrun-Timestamp`, `X-Foldrun-Signature` | when `signing_secret:` is set — see below |
236
237
 
@@ -32,9 +32,52 @@ curl -fsSL https://foldrun.io/install.sh | sh
32
32
 
33
33
  It checks Docker, puts `docker-compose.yml` in `~/foldrun`, writes `.env`
34
34
  there once (a fresh secret key, the address, the version), pulls the
35
- ready-made images and starts foldrun on port 3900. Then open
36
- `http://localhost:3900/signup` in a browser — the first account to
37
- sign up becomes the owner — then sign the CLI in and deploy:
35
+ ready-made images for your machine's chip and starts foldrun on port 3900.
36
+
37
+ **No Docker yet?** The installer offers to set it up, and asks before each
38
+ step (`[y/N]`):
39
+
40
+ | on | Docker missing | Docker stopped | Docker not reachable by you |
41
+ |---|---|---|---|
42
+ | **macOS** | installs Colima (a small Linux VM), `docker` and `docker-compose` with [Homebrew](https://brew.sh), then starts Colima with up to 4 CPUs and 8 GiB — enough for a `heavy` step | starts Colima, or opens Docker Desktop | — |
43
+ | **Linux** | runs Docker's official script ([get.docker.com](https://get.docker.com): the engine and the Compose plugin) as root, through `sudo` | `sudo systemctl start docker` | adds you to the `docker` group (root-equivalent), and uses `sudo` for the rest of that run; the group applies from your next login |
44
+
45
+ Answer after each question appears: anything typed ahead is lost while
46
+ Docker installs.
47
+
48
+ **What it checks first.** The install folder and every file Compose reads
49
+ there (`.env`, `docker-compose.yml`, `docker-compose.override.yml`) must be
50
+ yours alone — owned by you, writable by nobody else, no access control list,
51
+ no symlinks — because Docker runs what they say with root's reach. The
52
+ override must stand alone: no `include` or `extends`, and no `env_file` —
53
+ put settings in `.env` or in the override's own `environment:`. Anything
54
+ else it names, such as a secret's file, is your own configuration to
55
+ protect, as is a key written to slip past these checks.
56
+
57
+ `FOLDRUN_DATA_DIR` (a host folder instead of the data volume) holds the
58
+ accounts and the vault, so it must be private: the installer makes it
59
+ owner-only when it does not exist, refuses one that others can read or
60
+ write, and keeps its real path (symlinks resolved) in `.env`.
61
+
62
+ The installer sets foldrun up on the machine it runs on, and refuses a
63
+ remote Docker (`DOCKER_HOST` or a context pointing at another machine): run
64
+ it on the Docker host itself.
65
+ Compose is always given these files by name, so a `compose.yaml` beside them
66
+ or a `COMPOSE_FILE` in `.env` cannot stand in. Before starting, it checks
67
+ that the Docker the platform will be given (`FOLDRUN_DOCKER_SOCK`) is the
68
+ same daemon it installed into. With no terminal (a provisioning script), nothing is
69
+ installed unless you say so in advance with `FOLDRUN_INSTALL_DOCKER=yes`;
70
+ `FOLDRUN_INSTALL_DOCKER=no` never asks. Without Homebrew on a Mac, install
71
+ it or [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/)
72
+ first. Then open the signup link it prints —
73
+ `http://127.0.0.1:3900/signup?setup=…` (127.0.0.1, not `localhost`, which a browser may send to IPv6 where another user could be listening) — in a browser. The first account
74
+ becomes the owner, and only that link can create it: anyone else using the
75
+ same machine can reach `localhost` too, so the installer ties the owner to
76
+ whoever ran it (the token is `FOLDRUN_SETUP_TOKEN` in `~/foldrun/.env`, and
77
+ does nothing once the owner exists). With `FOLDRUN_BIND` set to one address
78
+ the link uses that address; from another machine, open it through the
79
+ tunnel the installer prints (`ssh -o ExitOnForwardFailure=yes -L …`), never
80
+ over plain http. Then sign the CLI in and deploy:
38
81
 
39
82
  ```sh
40
83
  npm install -g foldrun
@@ -67,6 +110,8 @@ edit `~/foldrun/.env` and run `docker compose up -d` there.
67
110
  | `ANTHROPIC_API_KEY` | none | the install's model key. Without one, each account adds its own in Settings → Model; a step with neither is refused with what to set |
68
111
  | `FOLDRUN_DIR` | `~/foldrun` | where the install lives (installer only) |
69
112
  | `FOLDRUN_DOCKER_SOCK` | `/var/run/docker.sock` | the Docker socket; rootless Docker is found by the installer |
113
+ | `FOLDRUN_SETUP_TOKEN` | made once | the first account (the owner) is created only with this, through the printed `/signup?setup=…` link |
114
+ | `FOLDRUN_INSTALL_DOCKER` | ask | `yes` installs or starts Docker without asking (for a script with no terminal); `no` never offers to |
70
115
  | `FOLDRUN_EGRESS_BIND` | `0.0.0.0` | the egress proxy's port 8090, which steps reach through `host.docker.internal`. On a server that faces a network, bind it to the Docker bridge (`172.17.0.1`); Docker-published ports skip host firewalls such as ufw |
71
116
  | `FOLDRUN_DATA_DIR` | a named volume | a host path for the data instead |
72
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldrun",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Agents are just folders. Write them, check them, run them — on your machine.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  "node": ">=22"
12
12
  },
13
13
  "dependencies": {
14
- "@foldrun/core": "^2.13.0"
14
+ "@foldrun/core": "^2.14.0"
15
15
  },
16
16
  "files": [
17
17
  "bin",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "e2e": "FOLDRUN_E2E=1 FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-e2e-XXXXXX) node --test --test-timeout=300000 tests/e2e.test.ts",
25
- "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-issues.test.ts tests/cli-capture.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-frontmatter.test.ts tests/cli-agent-import.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-agent-link-isolated.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-report-cache.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 tests/cli-flags.test.ts tests/cli-api-coverage.test.ts tests/cli-hooks.test.ts tests/cli-release.test.ts tests/cli-terminal.test.ts tests/cli-network.test.ts tests/cli-assistant.test.ts tests/cli-check-scan.test.ts tests/cli-published.test.ts tests/cli-init-target.test.ts tests/cli-flow-filter.test.ts tests/cli-eval-grades.test.ts tests/cli-eval-compare.test.ts tests/cli-proposals.test.ts tests/cli-inbox.test.ts",
25
+ "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-issues.test.ts tests/cli-capture.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-frontmatter.test.ts tests/cli-agent-import.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-agent-link-isolated.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-report-cache.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 tests/cli-flags.test.ts tests/cli-api-coverage.test.ts tests/cli-hooks.test.ts tests/cli-release.test.ts tests/cli-terminal.test.ts tests/cli-network.test.ts tests/cli-assistant.test.ts tests/cli-check-scan.test.ts tests/cli-published.test.ts tests/cli-init-target.test.ts tests/cli-flow-filter.test.ts tests/cli-eval-grades.test.ts tests/cli-eval-compare.test.ts tests/cli-proposals.test.ts tests/cli-inbox.test.ts tests/cli-usage-compute.test.ts",
26
26
  "release": "node scripts/release.mjs",
27
27
  "typecheck": "tsc -p tsconfig.json",
28
28
  "docs:sync": "node scripts/sync-docs.mjs",
package/src/commands.mjs CHANGED
@@ -5650,7 +5650,7 @@ function printDefaults(d) {
5650
5650
  console.log(` notify ${notify}`);
5651
5651
  console.log(` budget ${d.budget ? `$${d.budget.usd} per ${d.budget.period}` : c.dim("no cap")}`);
5652
5652
  console.log(` concurrency ${d.concurrency ?? c.dim("the plan's number")}`);
5653
- console.log(` compute ${d.compute ? `${d.compute.vcpu ?? "plan's"} vCPU · ${d.compute.memGiB ?? "plan's"} GiB ${c.dim("— at most, for all running steps")}` : c.dim("the plan's (foldrun usage compute)")}`);
5653
+ console.log(` compute ${d.compute ? `${d.compute.vcpu ?? "plan's"} vCPU · ${d.compute.memGiB ?? "plan's"} GiB · ${d.compute.diskGiB ?? "plan's"} GiB disk ${c.dim("— at most, for all running steps")}` : c.dim("the plan's (foldrun usage compute)")}`);
5654
5654
  console.log(` deploy_block ${d.deployBlock && d.deployBlock !== "none" ? `${d.deployBlock} ${c.dim(d.deployBlock === "secrets" ? "— a written-in key refuses a deploy" : "— any security finding refuses a deploy")}` : c.dim("none — security findings only warn")}`);
5655
5655
  console.log(` network ${networkEnforceText(d.networkEnforce, "the install")}`);
5656
5656
  }
@@ -5963,19 +5963,9 @@ async function accountCmd(positional, flags) {
5963
5963
  } else if (key === "alerts" || key === "judge") {
5964
5964
  patch = { [key]: await mergedEntry(url, flags, key, positional.slice(2), null, view, `foldrun account set ${key}`) };
5965
5965
  } else if (key === "compute") {
5966
- // `set compute vcpu 4 memory 8` (GiB) — either or both, over what is set.
5967
- const pairs = positional.slice(2);
5968
- const usage = "`foldrun account set compute vcpu <cores> memory <GiB>` — either or both";
5969
- if (!pairs.length || pairs.length % 2) throw new Error(usage);
5970
- const { defaults } = await remoteCall(url, flags, "/api/account");
5971
- const next = { vcpu: defaults?.compute?.vcpu ?? null, memGiB: defaults?.compute?.memGiB ?? null };
5972
- for (let i = 0; i < pairs.length; i += 2) {
5973
- const [k, v] = [pairs[i], pairs[i + 1]];
5974
- if (k !== "vcpu" && k !== "memory") throw new Error(`${usage} — not "${k}"`);
5975
- if (!/^\d+(\.\d+)?$/.test(v)) throw new Error(`compute ${k} is a number${k === "memory" ? " of GiB" : " of cores"} — not "${v}"`);
5976
- next[k === "vcpu" ? "vcpu" : "memGiB"] = Number(v);
5977
- }
5978
- patch = { compute: next };
5966
+ // `set compute vcpu 4 memory 8 disk 40` (GiB) — any of them; the
5967
+ // platform merges them over what is set.
5968
+ patch = { compute: computePairs(positional.slice(2), "`foldrun account set compute vcpu <cores> memory <GiB> disk <GiB>` — any of them") };
5979
5969
  } else if (key === "notify") {
5980
5970
  // `set notify email you@example.com` / `set notify url https://…`, and
5981
5971
  // --events to change what is notified on.
@@ -9627,10 +9617,57 @@ async function usageCmd(flags) {
9627
9617
  return 0;
9628
9618
  }
9629
9619
 
9620
+ /**
9621
+ * `vcpu 4 memory 8 disk 40` → { vcpu, memGiB, diskGiB } with only what was
9622
+ * given — the platform merges it over what is set (27 Oct 2026), so naming
9623
+ * one figure never clears the others. `memory`/`disk` are GiB.
9624
+ */
9625
+ function computePairs(pairs, usage) {
9626
+ if (!pairs.length || pairs.length % 2) throw new Error(usage);
9627
+ const names = { vcpu: "vcpu", memory: "memGiB", disk: "diskGiB" };
9628
+ /** @type {Record<string, number>} */
9629
+ const out = {};
9630
+ for (let i = 0; i < pairs.length; i += 2) {
9631
+ const [k, v] = [pairs[i], pairs[i + 1]];
9632
+ if (!(k in names)) throw new Error(`${usage} — not "${k}"`);
9633
+ if (!/^\d+(\.\d+)?$/.test(v)) throw new Error(`compute ${k} is a number${k === "vcpu" ? " of cores" : " of GiB"} — not "${v}"`);
9634
+ const n = Number(v);
9635
+ // The bounds core's readCompute holds a file to, said here too, so a
9636
+ // local edit never writes what `foldrun check` then refuses.
9637
+ if (k === "vcpu" && (n < 0.5 || !Number.isInteger(n * 2))) throw new Error(`compute vcpu is 0.5 or more, in halves — not ${v}`);
9638
+ if (k === "memory" && n < 0.25) throw new Error(`compute memory is 0.25 GiB (256Mi) or more — not ${v}`);
9639
+ if (k === "disk" && n < 1) throw new Error(`compute disk is 1 GiB or more — not ${v}`);
9640
+ out[names[k]] = n;
9641
+ }
9642
+ return out;
9643
+ }
9644
+
9645
+ /** A size as AGENTS.md writes it — 8Gi, 1536Mi, 8g — in GiB; null when none. */
9646
+ function quantityGiB(q) {
9647
+ const m = /^(\d+(?:\.\d+)?)\s*(Mi|Gi|m|g)$/.exec(String(q ?? "").trim());
9648
+ if (!m) return null;
9649
+ return m[2] === "Gi" || m[2] === "g" ? Number(m[1]) : Number(m[1]) / 1024;
9650
+ }
9651
+
9652
+ /** `compute:` as a file writes it ({vcpu, memory: 8Gi, disk: 40Gi}) in
9653
+ * numbers, or null when it is not one. */
9654
+ function computeOfFile(raw) {
9655
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
9656
+ const o = /** @type {Record<string, unknown>} */ (raw);
9657
+ return { vcpu: typeof o.vcpu === "number" ? o.vcpu : null, memGiB: quantityGiB(o.memory), diskGiB: quantityGiB(o.disk) };
9658
+ }
9659
+
9660
+ /** "4 vCPU · 8 GiB · 40 GiB disk", or the fallback when nothing is set. */
9661
+ function computeText(p, none) {
9662
+ if (!p) return none;
9663
+ const parts = [p.vcpu != null ? `${p.vcpu} vCPU` : null, p.memGiB != null ? `${p.memGiB} GiB` : null, p.diskGiB != null ? `${p.diskGiB} GiB disk` : null].filter(Boolean);
9664
+ return parts.length ? parts.join(" · ") : none;
9665
+ }
9666
+
9630
9667
  /**
9631
9668
  * `foldrun usage compute` — the cores and GiB the account's running steps
9632
- * may hold together and in one step, and what they hold now (Settings →
9633
- * Governance, GET /api/usage/compute).
9669
+ * may hold together and in one step, what they hold now, and the steps
9670
+ * waiting for room (Settings → Governance, GET /api/usage/compute).
9634
9671
  */
9635
9672
  async function usageComputeCmd(flags) {
9636
9673
  const url = platformFor(flags, "usage compute");
@@ -9640,13 +9677,39 @@ async function usageComputeCmd(flags) {
9640
9677
  return 0;
9641
9678
  }
9642
9679
  const p = d.compute ?? {};
9643
- const u = d.inUse ?? { vcpu: 0, memGiB: 0, steps: 0 };
9680
+ const u = d.inUse ?? { vcpu: 0, memGiB: 0, diskGiB: 0, steps: 0 };
9644
9681
  const of = (used, cap, unit) => (cap == null ? `${used} ${unit} ${c.dim("(no cap)")}` : `${used} of ${cap} ${unit}`);
9682
+ const held = (h, pool) => `${of(h.vcpu ?? 0, pool?.vcpu, "vCPU")} · ${of(h.memGiB ?? 0, pool?.memGiB, "GiB")} · ${of(h.diskGiB ?? 0, pool?.diskGiB, "GiB disk")}`;
9645
9683
  const from = { plan: "the plan's", account: "the account's own compute:", install: "this install's" }[p.source] ?? null;
9646
- console.log(`\n ${c.bold("compute")} ${c.dim(from ? `${from} — what all running steps may hold together` : "no pool set")}`);
9647
- console.log(` ${of(u.vcpu, p.vcpu, "vCPU")} · ${of(u.memGiB, p.memGiB, "GiB")} ${c.dim(`held by ${u.steps} running step${u.steps === 1 ? "" : "s"}`)}`);
9648
- if (p.stepVcpu != null || p.stepMemGiB != null) console.log(` ${c.dim(`one step: up to ${p.stepVcpu ?? "any"} vCPU · ${p.stepMemGiB ?? "any"} GiB`)}`);
9649
- console.log(`\n ${c.dim("a step past the pool waits for one to finish · lower it: foldrun account set compute vcpu <n> memory <GiB>")}\n`);
9684
+ console.log(`\n ${c.bold("account")} ${c.dim(from ? `${from} — what all running steps may hold together` : "no pool set")}`);
9685
+ console.log(` ${held(u, p)} ${c.dim(`held by ${u.steps} running step${u.steps === 1 ? "" : "s"}`)}`);
9686
+ if (p.stepVcpu != null || p.stepMemGiB != null || p.stepDiskGiB != null) console.log(` ${c.dim(`one step: up to ${p.stepVcpu ?? "any"} vCPU · ${p.stepMemGiB ?? "any"} GiB · ${p.stepDiskGiB ?? "any"} GiB disk`)}`);
9687
+ // Each workspace's own pool and each live run's (27 Oct 2026): a step
9688
+ // must fit every level it belongs to.
9689
+ const wss = Array.isArray(d.workspaces) ? d.workspaces.filter((w) => w.inUse?.steps || w.pool?.vcpu != null || w.pool?.memGiB != null || w.pool?.diskGiB != null) : [];
9690
+ if (wss.length) {
9691
+ console.log(`\n ${c.bold("workspaces")} ${c.dim("each one's own pool, inside the account's")}`);
9692
+ const width = Math.max(...wss.map((w) => w.workspace.length));
9693
+ for (const w of wss) console.log(` ${pad(w.workspace, width)} ${held(w.inUse ?? {}, w.pool)}`);
9694
+ }
9695
+ const runs = Array.isArray(d.runs) ? d.runs : [];
9696
+ if (runs.length) {
9697
+ console.log(`\n ${c.bold("runs")} ${c.dim("live, each with its flow's pool")}`);
9698
+ for (const r of runs.slice(0, 20)) console.log(` ${c.bold(`${r.workspace}/${r.flow}`)} ${c.dim(r.runId)} ${held(r.inUse ?? {}, r.pool)}`);
9699
+ }
9700
+ // Waiting for room (27 Oct 2026): oldest first, each starts in turn.
9701
+ const waiting = Array.isArray(d.waiting) ? d.waiting : [];
9702
+ if (waiting.length) {
9703
+ console.log(`\n ${c.bold(`${waiting.length} waiting for room`)} ${c.dim("oldest first — each starts in turn; not charged while it waits")}`);
9704
+ for (const w of waiting.slice(0, 20)) {
9705
+ const mins = Math.max(0, Math.round((Date.now() - Date.parse(w.since)) / 60_000));
9706
+ const age = mins < 120 ? `${mins} min` : `${Math.round(mins / 60)} h`;
9707
+ console.log(` ${pad(age, 7)} ${c.bold(`${w.workspace}/${w.flow}`)} ${w.agent} ${w.cpus} vCPU · ${w.memGiB} GiB${w.diskGiB ? ` · ${w.diskGiB} GiB disk` : ""} ${c.dim(w.runId)}${w.level ? c.dim(` · held by its ${w.level}`) : ""}`);
9708
+ console.log(` ${c.dim(w.why)}`);
9709
+ }
9710
+ if (waiting.length > 20) console.log(` ${c.dim(`and ${waiting.length - 20} more — --json for all`)}`);
9711
+ }
9712
+ console.log(`\n ${c.dim("a step past any pool waits, as long as it takes, and starts in turn · set them: foldrun account set compute … · foldrun workspace set compute … --to <ws> · foldrun flow compute <flow> …")}\n`);
9650
9713
  return 0;
9651
9714
  }
9652
9715
 
@@ -9826,7 +9889,7 @@ async function workspaceCmd(positional, flags, layout) {
9826
9889
  const [verb, written, ...rest] = positional;
9827
9890
  // `observability.notify` names the job; checked, then sent as the field.
9828
9891
  const { key, view } = verb === "set" || verb === "clear" ? await jobKey(url, flags, written, ws) : { key: written, view: null };
9829
- const KEYS = ["name", "description", "timezone", "budget", "notify", "network_enforce", "owner", "alerts", "judge"];
9892
+ const KEYS = ["name", "description", "timezone", "budget", "compute", "notify", "network_enforce", "owner", "alerts", "judge"];
9830
9893
  const show = (d, title) => {
9831
9894
  const notify = d.notify ? [d.notify.email, d.notify.url].filter(Boolean).join(", ") + c.dim(` on ${(d.notify.events ?? []).join(", ") || "nothing"}`) : c.dim("the account's");
9832
9895
  const budget = d.budget == null ? c.dim("the account's") : typeof d.budget === "object" ? `$${d.budget.usd} per ${d.budget.period}` : String(d.budget).includes("/") ? `$${String(d.budget).replace("/", " per ")}` : `$${d.budget} per month`;
@@ -9835,12 +9898,13 @@ async function workspaceCmd(positional, flags, layout) {
9835
9898
  console.log(` timezone ${d.timezone ?? c.dim("the account's")}`);
9836
9899
  console.log(` notify ${notify}`);
9837
9900
  console.log(` budget ${budget}`);
9901
+ console.log(` compute ${computeText(d.compute, c.dim("no cap of its own — the account's"))}`);
9838
9902
  if (d.networkEnforce !== undefined) console.log(` network ${networkEnforceText(d.networkEnforce, "the account")}`);
9839
9903
  };
9840
9904
 
9841
9905
  if (!verb) {
9842
9906
  const d = await workspaceDefaults(url, flags, ws);
9843
- show({ description: typeof d.description === "string" ? d.description : "", timezone: d.timezone ?? null, budget: d.budget ?? null, notify: d.notify ?? null, networkEnforce: d.network_enforce === true }, ws);
9907
+ show({ description: typeof d.description === "string" ? d.description : "", timezone: d.timezone ?? null, budget: d.budget ?? null, notify: d.notify ?? null, networkEnforce: d.network_enforce === true, compute: computeOfFile(d.compute) }, ws);
9844
9908
  const halt = await pauseState(remoteCall(url, flags, `/api/workspaces/${enc(ws)}/pause`));
9845
9909
  if (halt) console.log(` paused ${halt.paused ? c.red(`${halt.paused.scope === "account" ? "by the account: " : ""}${pauseLine(halt.paused)}`) : c.dim("no")}`);
9846
9910
  console.log(`\n ${c.dim(`foldrun workspace set timezone Australia/Sydney --to ${ws} · set budget 20/week · set notify email you@example.com · set name <new> · clear budget`)}\n`);
@@ -9862,6 +9926,9 @@ async function workspaceCmd(positional, flags, layout) {
9862
9926
  if (verb === "clear") patch = { [key]: key === "description" ? "" : null };
9863
9927
  else if (key === "alerts" || key === "judge") {
9864
9928
  patch = { [key]: await mergedEntry(url, flags, key, rest, ws, view, `foldrun workspace set ${key}`) };
9929
+ } else if (key === "compute") {
9930
+ // This workspace's pool (27 Oct 2026) — it only narrows the account's.
9931
+ patch = { compute: computePairs(rest, `\`foldrun workspace set compute vcpu <cores> memory <GiB> disk <GiB> --to ${ws}\` — any of them`) };
9865
9932
  } else if (key === "notify") {
9866
9933
  const [channel, value] = rest;
9867
9934
  if (channel !== "email" && channel !== "url") throw new Error("`foldrun workspace set notify email <address>` or `set notify url <https://…>` — and --events failed,awaiting-approval,completed");
@@ -10436,6 +10503,63 @@ async function flowTriggerCmd(positional, flags, layout) {
10436
10503
  });
10437
10504
  }
10438
10505
 
10506
+ /**
10507
+ * `foldrun flow compute <flow> vcpu 2 memory 4 disk 20` — what one run of the
10508
+ * flow may hold at once across its parallel steps (27 Oct 2026; the flow's
10509
+ * `compute:`); any of the three, merged over what is set; `--clear` for
10510
+ * none. On the platform it is the flows route's { compute } (the flow
10511
+ * page's card); here, the flow file's frontmatter.
10512
+ */
10513
+ async function flowComputeCmd(positional, flags, layout) {
10514
+ const flow = positional[1];
10515
+ const usage = "`foldrun flow compute <flow> vcpu <cores> memory <GiB> disk <GiB>` — any of them; `--clear` for no cap of its own";
10516
+ if (!flow) throw new Error(usage);
10517
+ const asked = flags.clear === true ? null : computePairs(positional.slice(2), usage);
10518
+ const dry = flags["dry-run"] === true;
10519
+ if (namesPlatform(flags)) {
10520
+ // On the platform: the route merges, so nothing needs reading first.
10521
+ const url = platformFor(flags, "flow compute");
10522
+ const ws = platformWorkspace(flags, layout, "flow compute");
10523
+ if (dry) {
10524
+ console.log(`\n ${c.bold(flow)} ${c.dim(`in ${ws}`)} — would send compute ${JSON.stringify(asked)} (merged over what the flow says)\n ${c.dim("--dry-run: nothing sent")}\n`);
10525
+ return 0;
10526
+ }
10527
+ const d = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/flows/${enc(flow)}`, { method: "PATCH", body: JSON.stringify({ compute: asked }) });
10528
+ console.log(`\n ${c.green("✓")} ${c.bold(flow)} ${c.dim(`in ${ws}`)} — each run may hold ${computeText(d.flow?.compute, "no cap of its own")}\n`);
10529
+ return 0;
10530
+ }
10531
+ const desk = await openDesk(flags, layout, `flow compute ${flow}`, flow);
10532
+ const rel = await flowFileOf(desk, flow);
10533
+ const raw = desk.files.get(rel);
10534
+ const { patchFrontmatter, readAgentsMdKeys } = await import("@foldrun/core/agents-md");
10535
+ // The flow's frontmatter now, read the way AGENTS.md is (a temp copy).
10536
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "foldrun-flow-"));
10537
+ let current = {};
10538
+ try {
10539
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), raw);
10540
+ current = readAgentsMdKeys(dir).compute ?? {};
10541
+ } finally {
10542
+ fs.rmSync(dir, { recursive: true, force: true });
10543
+ }
10544
+ /** @type {Record<string, unknown> | null} */
10545
+ let value = null;
10546
+ if (asked) {
10547
+ value = typeof current === "object" && current && !Array.isArray(current) ? { ...current } : {};
10548
+ if (asked.vcpu !== undefined) value.vcpu = asked.vcpu;
10549
+ if (asked.memGiB !== undefined) value.memory = `${asked.memGiB}Gi`;
10550
+ if (asked.diskGiB !== undefined) value.disk = `${asked.diskGiB}Gi`;
10551
+ }
10552
+ const next = patchFrontmatter(raw, { compute: value });
10553
+ if (dry) {
10554
+ await printDiff(rel, raw, next);
10555
+ console.log(`\n ${c.dim("--dry-run: nothing written")}\n`);
10556
+ return 0;
10557
+ }
10558
+ writeIfUnchanged(desk, rel, raw, next);
10559
+ console.log(`\n ${c.green("✓")} ${rel} — each run may hold ${computeText(computeOfFile(value), "no cap of its own")}\n`);
10560
+ return 0;
10561
+ }
10562
+
10439
10563
  /**
10440
10564
  * `foldrun flow filter <flow> --payload <file|-|json> [--to <workspace>]` —
10441
10565
  * would this delivery start the flow? Its `filter:` (core trigger-filter.ts)
@@ -11954,6 +12078,7 @@ export async function run(command, positional, flags, workspace, layout) {
11954
12078
  if (positional[0] === "hook") return flowHookCmd(positional, flags, layout);
11955
12079
  if (positional[0] === "set") return flowSetCmd(positional, flags, layout);
11956
12080
  if (positional[0] === "trigger") return flowTriggerCmd(positional, flags, layout);
12081
+ if (positional[0] === "compute") return flowComputeCmd(positional, flags, layout);
11957
12082
  if (positional[0] === "filter") return flowFilterCmd(positional, flags, layout);
11958
12083
  if (positional[0] === "move-step") return flowMoveStepCmd(positional, flags, layout);
11959
12084
  if (positional[0] === "copy-step") return flowCopyStepCmd(positional, flags, layout);
package/src/guide.mjs CHANGED
@@ -30,7 +30,7 @@ export function agentRulesBlock() {
30
30
 
31
31
  # Read foldrun's docs before you edit this folder
32
32
 
33
- This is a foldrun account: agents, flows, tools and knowledge as markdown, run by foldrun. Its formats — frontmatter fields, step options, \`verify:\` prefixes, which folder a file goes in — are foldrun's own and may differ from your training data. Before you write or change an agent, flow, tool or skill, read the relevant guide: \`foldrun docs\` lists them and \`foldrun docs <page>\` prints one (start with \`foldrun docs coding-agents\`). They ship with the CLI, so they match the version you are driving. Check every edit with \`foldrun check\` — offline, no model calls; it is also the security scanner (a key written into a file, hidden text, an agent that can leak), and \`foldrun check --security\` fails on what it finds. Never paste a credential into a file: \`foldrun secrets set NAME\` and write \`\${NAME}\`. For a flow's shape, \`foldrun flow show <flow>\` draws it, and \`foldrun flow add\`, \`foldrun flow rm-step\`, \`foldrun flow dup-step\` and \`foldrun agent link\` make the same edits as the dashboard canvas (patterns, sub-agents, consults, \`ask\`). An agent with \`tools: [peers]\` can message the steps running beside it in its parallel group (\`peers_message\`) and ask for what they have instead of guessing or failing (\`foldrun docs agents\`). A sub-agent that reads untrusted content (web pages, mail, uploads) gets \`sandbox: own\` in its own file: each job then runs in a sandbox of its own with its own tools, secrets and network, and only its reply and the files it wrote reach the agent that delegated (\`foldrun agent link <agent> --subagent <worker> --isolated\`). A script tool's own \`secrets: [NAMES]\` are granted with the tool; list on an agent only the secrets it uses directly. A hook (\`hooks/<name>/hook.md\` + \`run.mjs\`, turned on with \`hooks: [name]\`) is code at every tool call, step start and step end — \`foldrun hook add\`/\`test\` and \`foldrun docs hooks\`. Paths to the workspace's own folders are written \`workspace/storage/…\` and \`workspace/state/…\` — in an agent's prose, a flow's options and a script's arguments — not \`../../\`, which still works but is the older spelling. Which sites a step's sandbox may reach is \`network:\` — open for an agent that may fetch or browse the web, the trusted list plus what it declares for every other; add a site with \`foldrun agent network <agent> --add <site>\` (\`foldrun docs network\`). Bind a secret to the sites it is for (\`foldrun secrets sites NAME <site>\`) and no sandbox ever holds its value. What a step's sandbox may use is \`size:\` (\`small\` 1 core/1Gi, \`large\`, \`heavy\` 4 cores/8Gi — hard limits and the billed figures); \`resources: {cpu, memory, disk}\` sets the numbers yourself, key by key over the class (\`foldrun docs agents\`). A webhook, email, chained or storage flow says which deliveries start it with \`filter:\` (\`foldrun flow filter <flow> --payload body.json\` tries one); an eval with \`trigger: runs\` grades a \`sample:\` of real finished runs instead of running cases, \`foldrun eval compare\` runs an eval's cases under several models side by side, and \`improve: true\` turns repeated failures into a proposed fix a person applies (\`foldrun docs evals\`).
33
+ This is a foldrun account: agents, flows, tools and knowledge as markdown, run by foldrun. Its formats — frontmatter fields, step options, \`verify:\` prefixes, which folder a file goes in — are foldrun's own and may differ from your training data. Before you write or change an agent, flow, tool or skill, read the relevant guide: \`foldrun docs\` lists them and \`foldrun docs <page>\` prints one (start with \`foldrun docs coding-agents\`). They ship with the CLI, so they match the version you are driving. Check every edit with \`foldrun check\` — offline, no model calls; it is also the security scanner (a key written into a file, hidden text, an agent that can leak), and \`foldrun check --security\` fails on what it finds. Never paste a credential into a file: \`foldrun secrets set NAME\` and write \`\${NAME}\`. For a flow's shape, \`foldrun flow show <flow>\` draws it, and \`foldrun flow add\`, \`foldrun flow rm-step\`, \`foldrun flow dup-step\` and \`foldrun agent link\` make the same edits as the dashboard canvas (patterns, sub-agents, consults, \`ask\`). An agent with \`tools: [peers]\` can message the steps running beside it in its parallel group (\`peers_message\`) and ask for what they have instead of guessing or failing (\`foldrun docs agents\`). A sub-agent that reads untrusted content (web pages, mail, uploads) gets \`sandbox: own\` in its own file: each job then runs in a sandbox of its own with its own tools, secrets and network, and only its reply and the files it wrote reach the agent that delegated (\`foldrun agent link <agent> --subagent <worker> --isolated\`). A script tool's own \`secrets: [NAMES]\` are granted with the tool; list on an agent only the secrets it uses directly. A hook (\`hooks/<name>/hook.md\` + \`run.mjs\`, turned on with \`hooks: [name]\`) is code at every tool call, step start and step end — \`foldrun hook add\`/\`test\` and \`foldrun docs hooks\`. Paths to the workspace's own folders are written \`workspace/storage/…\` and \`workspace/state/…\` — in an agent's prose, a flow's options and a script's arguments — not \`../../\`, which still works but is the older spelling. Which sites a step's sandbox may reach is \`network:\` — open for an agent that may fetch or browse the web, the trusted list plus what it declares for every other; add a site with \`foldrun agent network <agent> --add <site>\` (\`foldrun docs network\`). Bind a secret to the sites it is for (\`foldrun secrets sites NAME <site>\`) and no sandbox ever holds its value. What a step's sandbox may use is \`size:\` (\`small\` 1 core/1Gi, \`large\`, \`heavy\` 4 cores/8Gi — hard limits and the billed figures); \`resources: {cpu, memory, disk}\` sets the numbers yourself, key by key over the class (\`foldrun docs agents\`). What steps may hold together is capped at four levels, each only narrowing the one above: the account's and a workspace's \`compute: {vcpu, memory, disk}\` in AGENTS.md, a flow's \`compute:\` for each of its runs, and the plan's most for one step; a step past a cap waits its turn, one bigger than a whole cap fails (\`foldrun usage compute\`, \`foldrun docs budgets-and-billing\`). A webhook, email, chained or storage flow says which deliveries start it with \`filter:\` (\`foldrun flow filter <flow> --payload body.json\` tries one); an eval with \`trigger: runs\` grades a \`sample:\` of real finished runs instead of running cases, \`foldrun eval compare\` runs an eval's cases under several models side by side, and \`improve: true\` turns repeated failures into a proposed fix a person applies (\`foldrun docs evals\`).
34
34
 
35
35
  This block is for coding tools. It is written and re-added by the foldrun CLI, and the runtime strips it, so the agents that run here never see it; the rest of this file is the account's own context, which they do read. Committing the block with your work keeps the tree clean.
36
36